From 070867d965738a215f112b534647e883614b7a70 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Wed, 16 Aug 2023 23:07:29 +0300 Subject: [PATCH 01/80] Check for closest known opponent in status black --- TacticalAI/DecideAction.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index eb421ce88..e3006a2f1 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -4917,6 +4917,11 @@ INT16 ubMinAPCost; DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"DecideActionBlack"); + INT32 sOpponentGridNo; + INT8 bOpponentLevel; + sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel); + DebugAI(AI_MSG_INFO, pSoldier, String("sClosestOpponent %d", sClosestOpponent)); + // sevenfm: disable stealth mode pSoldier->bStealthMode = FALSE; // disable reverse movement mode From d99b12cb5f6339cf0fa587c63f1840e70f73f1b8 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Wed, 16 Aug 2023 23:08:03 +0300 Subject: [PATCH 02/80] Allow status black cover advance with full AP --- TacticalAI/DecideAction.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index e3006a2f1..93b3b6f6b 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -5975,7 +5975,7 @@ INT16 ubMinAPCost; // Black cover advance if (SoldierAI(pSoldier) && gfTurnBasedAI && - !pSoldier->bActionPoints == pSoldier->bInitialActionPoints && + //!pSoldier->bActionPoints == pSoldier->bInitialActionPoints && pSoldier->bInitialActionPoints > APBPConstants[AP_MINIMUM] && !gfHiddenInterrupt && !gTacticalStatus.fInterruptOccurred && From e5027278282828e4e26c6ddd9fd4273eccd2d6da Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 17 Aug 2023 19:41:08 +0300 Subject: [PATCH 03/80] Disable canceling AI actions for escorted mercs Logging AI decisions shows that this conditional would constantly cancel AI actions during regular gameplay. A special case like this should not affect normal AI routines and I'd rather disable this for now to prevent it from masking other problems with AI decisions. --- TacticalAI/AIMain.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index f60ab7557..8952ee147 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -651,6 +651,9 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named { // traversing offmap, ignore new situations } +// FIXME: Disabled temporarily to prevent AI actions constantly being cancelled during normal turn based combat. +// Need to find out when this conditional is actually needed. +#if 0 else if ( pSoldier->ubQuoteRecord == 0 && !gTacticalStatus.fAutoBandageMode ) { // don't force, don't want escorted mercs reacting to new opponents, etc. @@ -664,6 +667,7 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named } DecideAlertStatus( pSoldier ); } +#endif else { if ( pSoldier->ubQuoteRecord ) From bacad9b4c023ccf06088881c0837574ca56a09b9 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 17 Aug 2023 19:41:41 +0300 Subject: [PATCH 04/80] Prevent AI deadlock when they try to shoot and player gets an interrupt --- TacticalAI/AIMain.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 8952ee147..827515f58 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -675,6 +675,13 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named // make sure we're not using combat AI pSoldier->aiData.bAlertStatus = STATUS_GREEN; } + // Poor hack to prevent AI deadlocking in case they change stance before firing and player gets an interrupt. + // Without this, if player doesn't move any mercs, the AI soldier won't fire and will wait until the deadlock is broken + // By canceling the AI action, the AI can reconsider actions and oddly enough, usually decides to fire but this time successfully. + if (pSoldier->aiData.bAction == AI_ACTION_FIRE_GUN && pSoldier->aiData.bLastAction == AI_ACTION_NONE) + { + CancelAIAction(pSoldier, FALSE); + } pSoldier->aiData.bNewSituation = WAS_NEW_SITUATION; } } From 0e529070c4807d36ca29b9c3748ba2a4309bfafb Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 17 Aug 2023 19:42:43 +0300 Subject: [PATCH 05/80] Allow AI to shoot with lower aim if out of AP for current aim level --- TacticalAI/AIMain.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 827515f58..6aae43e67 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -2299,6 +2299,17 @@ INT8 ExecuteAction(SOLDIERTYPE *pSoldier) usHandItem = GetAttachedGrenadeLauncher(&pSoldier->inv[HANDPOS]); iRetCode = HandleItem( pSoldier, pSoldier->aiData.usActionData, pSoldier->bTargetLevel, usHandItem, FALSE ); + // If AI cannot shoot because of lack of APs, attempt to try again with lower aim. + // Usually happens when they have to turn before shooting. Without this, the game would cancel soldier's whole turn + if (iRetCode == ITEM_HANDLE_NOAPS && pSoldier->aiData.bAimTime > 0) + { + do + { + pSoldier->aiData.bAimTime -= 1; + iRetCode = HandleItem(pSoldier, pSoldier->aiData.usActionData, pSoldier->bTargetLevel, usHandItem, FALSE); + } while (iRetCode == ITEM_HANDLE_NOAPS && pSoldier->aiData.bAimTime > 0); + } + if ( iRetCode != ITEM_HANDLE_OK) { if ( iRetCode != ITEM_HANDLE_BROKEN ) // if the item broke, this is 'legal' and doesn't need reporting From 1c8d008a668e1439f41e29e0ebb1ff61b870a5cb Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 17 Aug 2023 19:49:55 +0300 Subject: [PATCH 06/80] Move DebugAI() calls inside if blocks Clutters logs and slows down game less when logging is on. --- TacticalAI/DecideAction.cpp | 46 ++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 93b3b6f6b..9e5ea8524 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -2729,13 +2729,10 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) DebugAI(AI_MSG_TOPIC, pSoldier, String("[CheckIfTossPossible]")); CheckIfTossPossible(pSoldier,&BestThrow); - if (BestThrow.ubPossible) - DebugAI(AI_MSG_INFO, pSoldier, String("throw possible")); - else - DebugAI(AI_MSG_INFO, pSoldier, String("throw not possible")); if (BestThrow.ubPossible) { + DebugAI(AI_MSG_INFO, pSoldier, String("throw possible")); // sevenfm: allow using mortars, grenade launchers, flares and grenades in RED state if (Item[pSoldier->inv[BestThrow.bWeaponIn].usItem].mortar || //Item[pSoldier->inv[ BestThrow.bWeaponIn ].usItem].cannon || @@ -2838,6 +2835,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } else // toss/throw/launch not possible { + DebugAI(AI_MSG_INFO, pSoldier, String("throw not possible")); // WDS - Fix problem when there is no "best thrown" weapon (i.e., BestThrow.bWeaponIn == NO_SLOT) // if this dude has a longe-range weapon on him (longer than normal // sight range), and there's at least one other team-mate around, and @@ -2860,7 +2858,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } // use smoke to cover friend - DebugAI(AI_MSG_TOPIC, pSoldier, String("[use smoke to cover friend]")); if (gfTurnBasedAI && SoldierAI(pSoldier) && !bInWater && @@ -2875,6 +2872,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) Chance(100 - min(100, 10 * CountPublicKnownEnemies(pSoldier, pSoldier->sGridNo, TACTICAL_RANGE))) && !GuySawEnemy(pSoldier, SEEN_LAST_TURN)) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[use smoke to cover friend]")); DebugAI(AI_MSG_INFO, pSoldier, String("check if we can cover friend with smoke")); CheckTossFriendSmoke(pSoldier, &BestThrow); @@ -3435,7 +3433,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //////////////////////////////////////////////////////////////////////// // RED RETREAT //////////////////////////////////////////////////////////////////////// - DebugAI(AI_MSG_TOPIC, pSoldier, String("[retreat]")); if (gfTurnBasedAI && !fCivilian && !bInWater && @@ -3446,6 +3443,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->RetreatCounterValue() > 0 && (pSoldier->CheckInitialAP() || !fAnyCover || pSoldier->aiData.bUnderFire)) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[retreat]")); DebugAI(AI_MSG_TOPIC, pSoldier, String("search for retreat spot")); INT32 sRetreatSpot = FindRetreatSpot(pSoldier); @@ -3612,7 +3610,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Self smoke when under fire]")); if (gfTurnBasedAI && pSoldier->bActionPoints == pSoldier->bInitialActionPoints && pSoldier->aiData.bUnderFire && @@ -3625,6 +3622,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) (!fProneSightCover && !AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || pSoldier->TakenLargeHit()) && (pSoldier->TakenLargeHit() || pSoldier->ShockLevelPercent() > 20 + Random(80))) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Self smoke when under fire]")); DebugAI(AI_MSG_INFO, pSoldier, String("check if soldier can cover himself with smoke")); CheckTossSelfSmoke(pSoldier, &BestThrow); @@ -3821,7 +3819,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Set watched location]")); if (pSoldier->CheckInitialAP() && pSoldier->bActionPoints >= APBPConstants[AP_MINIMUM] && gfTurnBasedAI && @@ -3837,6 +3834,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) !SoldierToVirtualSoldierLineOfSightTest(pSoldier, sClosestDisturbance, pSoldier->pathing.bLevel, ANIM_STAND, TRUE, CALC_FROM_ALL_DIRS) && CountFriendsBlack(pSoldier, sClosestDisturbance) == 0) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Set watched location]")); gubNPCAPBudget = 0; gubNPCDistLimit = 0; @@ -5295,7 +5293,6 @@ INT16 ubMinAPCost; } } - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Self smoke when under fire]")); if (SoldierAI(pSoldier) && gfTurnBasedAI && pSoldier->bActionPoints == pSoldier->bInitialActionPoints && @@ -5309,6 +5306,7 @@ INT16 ubMinAPCost; (!ProneSightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE) && !AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || pSoldier->TakenLargeHit()) && (pSoldier->TakenLargeHit() || pSoldier->ShockLevelPercent() > 20 + Random(80))) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Self smoke when under fire]")); DebugAI(AI_MSG_INFO, pSoldier, String("check if soldier can cover himself with smoke")); CheckTossSelfSmoke(pSoldier, &BestThrow); @@ -5944,7 +5942,6 @@ INT16 ubMinAPCost; UINT16 usRange = BestAttack.bWeaponIn==NO_SLOT ? 0 : GetModifiedGunRange(pSoldier->inv[BestAttack.bWeaponIn].usItem);//dnl ch69 150913 INT32 sClosestThreat = ClosestKnownOpponent(pSoldier, NULL, NULL); - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Black Retreat]")); if (gfTurnBasedAI && !bInWater && ubCanMove && @@ -5957,6 +5954,7 @@ INT16 ubMinAPCost; (ubBestAttackAction == AI_ACTION_NONE || ubBestAttackAction == AI_ACTION_FIRE_GUN && (UINT8)BestAttack.ubChanceToReallyHit < Random(10 + pSoldier->ShockLevelPercent() / 4)) && (pSoldier->CheckInitialAP() || !AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || pSoldier->aiData.bUnderFire)) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Black Retreat]")); DebugAI(AI_MSG_TOPIC, pSoldier, String("search for retreat spot")); INT32 sRetreatSpot = FindRetreatSpot(pSoldier); @@ -5971,7 +5969,6 @@ INT16 ubMinAPCost; } } - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Black cover advance]")); // Black cover advance if (SoldierAI(pSoldier) && gfTurnBasedAI && @@ -5999,6 +5996,7 @@ INT16 ubMinAPCost; ubBestAttackAction == AI_ACTION_FIRE_GUN && BestAttack.ubChanceToReallyHit == 1 || !AnyCoverAtSpot(pSoldier, pSoldier->sGridNo))) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Black cover advance]")); DebugAI(AI_MSG_INFO, pSoldier, String("find cover advance spot")); INT32 sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb); @@ -6128,7 +6126,6 @@ INT16 ubMinAPCost; } } - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Allow taking cover]")); if ( (pSoldier->bActionPoints == pSoldier->bInitialActionPoints) && (ubBestAttackAction == AI_ACTION_FIRE_GUN) && (pSoldier->aiData.bShock == 0) && @@ -6137,6 +6134,7 @@ INT16 ubMinAPCost; (PythSpacesAway( pSoldier->sGridNo, BestAttack.sTarget ) > usRange / CELL_X_SIZE ) && (RangeChangeDesire( pSoldier ) >= 4) ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Allow taking cover]")); // okay, really got to wonder about this... could taking cover be an option? if (ubCanMove && pSoldier->aiData.bOrders != STATIONARY && !gfHiddenInterrupt && !(pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) ) @@ -6166,7 +6164,6 @@ INT16 ubMinAPCost; } DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"LOOK FOR SOME KIND OF COVER BETTER THAN WHAT WE HAVE NOW"); - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Find cover]")); //////////////////////////////////////////////////////////////////////////// // LOOK FOR SOME KIND OF COVER BETTER THAN WHAT WE HAVE NOW //////////////////////////////////////////////////////////////////////////// @@ -6181,6 +6178,7 @@ INT16 ubMinAPCost; !(pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) ) || fAllowCoverCheck ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Find cover]")); // sevenfm: if not found yet if(TileIsOutOfBounds(sBestCover)) { @@ -6195,7 +6193,6 @@ INT16 ubMinAPCost; #endif DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"DecideActionBlack: DECIDE BETWEEN ATTACKING AND DEFENDING (TAKING COVER)"); - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide attack/cover]")); ////////////////////////////////////////////////////////////////////////// // IF NECESSARY, DECIDE BETWEEN ATTACKING AND DEFENDING (TAKING COVER) ////////////////////////////////////////////////////////////////////////// @@ -6203,6 +6200,7 @@ INT16 ubMinAPCost; // if both are possible if ((ubBestAttackAction != AI_ACTION_NONE) && ( !TileIsOutOfBounds(sBestCover))) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide attack/cover]")); // gotta compare their merits and select the more desirable option iOffense = BestAttack.ubChanceToReallyHit; iDefense = iCoverPercentBetter; @@ -6272,11 +6270,11 @@ INT16 ubMinAPCost; } DebugMsg (TOPIC_JA2,DBG_LEVEL_3,String("DecideActionBlack: is attack still desirable? ubBestAttackAction = %d",ubBestAttackAction)); - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Attack]")); // if attack is still desirable (meaning it's also preferred to taking cover) if (ubBestAttackAction != AI_ACTION_NONE) { + //DebugAI(AI_MSG_TOPIC, pSoldier, String("[Attack]")); DebugAI(AI_MSG_TOPIC, pSoldier, String("[Prepare attack]")); // if we wanted to be REALLY mean, we could look at chance to hit and decide whether // to shoot at the head... @@ -6695,6 +6693,7 @@ INT16 ubMinAPCost; { pSoldier->aiData.usActionData = BestAttack.sTarget; pSoldier->bTargetLevel = BestAttack.bTargetLevel; + DebugAI(AI_MSG_INFO, pSoldier, String("Fire weapon!")); return(AI_ACTION_FIRE_GUN); } } @@ -6736,10 +6735,10 @@ INT16 ubMinAPCost; } } - DebugAI(AI_MSG_TOPIC, pSoldier, String("[End of Tank AI]")); // end of tank AI if ( !gGameExternalOptions.fEnemyTanksCanMoveInTactical && ARMED_VEHICLE( pSoldier ) ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[End of Tank AI]")); return( AI_ACTION_NONE ); } @@ -6832,7 +6831,6 @@ INT16 ubMinAPCost; } } - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Make boxer close if possible]")); // try to make boxer close if possible if (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER ) { @@ -6982,10 +6980,10 @@ INT16 ubMinAPCost; //////////////////////////////////////////////////////////////////////////// // IF A LOCATION WITH BETTER COVER IS AVAILABLE & REACHABLE, GO FOR IT! //////////////////////////////////////////////////////////////////////////// - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Take cover]")); if (!TileIsOutOfBounds(sBestCover)) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Take cover]")); #ifdef DEBUGDECISIONS STR tempstr=""; sprintf ( tempstr,"%s - TAKING COVER at gridno %d (%d%% better)\n", @@ -7008,10 +7006,10 @@ INT16 ubMinAPCost; // IF THINGS ARE REALLY HOPELESS, OR UNARMED, TRY TO RUN AWAY //////////////////////////////////////////////////////////////////////////// - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Run away]")); // if soldier has enough APs left to move at least 1 square's worth if ( ubCanMove && (pSoldier->bTeam != gbPlayerNum || pSoldier->aiData.fAIFlags & AI_RTP_OPTION_CAN_RETREAT) ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Run away]")); if ((pSoldier->aiData.bAIMorale == MORALE_HOPELESS) || !bCanAttack) { // look for best place to RUN AWAY to (farthest from the closest threat) @@ -7034,7 +7032,6 @@ INT16 ubMinAPCost; // IF SPOTTERS HAVE BEEN CALLED FOR, AND WE HAVE SOME NEW SIGHTINGS, RADIO! //////////////////////////////////////////////////////////////////////////// - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Radio sightings]")); // if we're a computer merc, and we have the action points remaining to RADIO // (we never want NPCs to choose to radio if they would have to wait a turn) // and we're not swimming in deep water, and somebody has called for spotters @@ -7043,6 +7040,7 @@ INT16 ubMinAPCost; (pSoldier->aiData.bOppCnt > 1) && !fCivilian && (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1) && !bInDeepWater) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Radio sightings]")); // base chance depends on how much new info we have to radio to the others iChance = 25 * WhatIKnowThatPublicDont(pSoldier,TRUE); // just count them @@ -7068,10 +7066,10 @@ INT16 ubMinAPCost; //////////////////////////////////////////////////////////////////////////// // CROUCH IF NOT CROUCHING ALREADY //////////////////////////////////////////////////////////////////////////// - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Crouch if not crouching already]")); // if not in water and not already crouched, try to crouch down first if (!gfTurnBasedAI || GetAPsToChangeStance( pSoldier, ANIM_CROUCH ) <= pSoldier->bActionPoints) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Crouch if not crouching already]")); if ( !fCivilian && !gfHiddenInterrupt && IsValidStance( pSoldier, ANIM_CROUCH ) && ubBestAttackAction != AI_ACTION_KNIFE_MOVE && ubBestAttackAction != AI_ACTION_KNIFE_STAB && ubBestAttackAction != AI_ACTION_STEAL_MOVE) // SANDRO - if knife attack don't crouch { // determine the location of the known closest opponent @@ -7136,9 +7134,9 @@ INT16 ubMinAPCost; //////////////////////////////////////////////////////////////////////////// // TURN TO FACE CLOSEST KNOWN OPPONENT (IF NOT FACING THERE ALREADY) //////////////////////////////////////////////////////////////////////////// - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Turn to closest known opponent]")); if (!gfTurnBasedAI || GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Turn to closest known opponent]")); // hopeless guys shouldn't waste their time this way, UNLESS they CAN move // but chose not to to get this far (which probably means they're cornered) // ALSO, don't bother turning if we're already aiming a gun @@ -7177,9 +7175,9 @@ INT16 ubMinAPCost; //////////////////////////////////////////////////////////////////////////// // RADIO RED ALERT: determine %chance to call others and report contact //////////////////////////////////////////////////////////////////////////// - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Report contacts]")); if ( !(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT) && pSoldier->bTeam == MILITIA_TEAM && (pSoldier->bActionPoints >= APBPConstants[AP_RADIO]) && (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1) ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Report contacts]")); // if there hasn't been an initial RED ALERT yet in this sector if ( !(gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition) || NeedToRadioAboutPanicTrigger() ) @@ -10349,7 +10347,7 @@ extern UINT32 guiArrived; void LogDecideInfo(SOLDIERTYPE *pSoldier) { - DebugAI(AI_MSG_INFO, pSoldier, String("Turn num %d aware %d", guiTurnCnt, gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition)); + DebugAI(AI_MSG_INFO, pSoldier, String("Turn num %d aware %d ubID %d", guiTurnCnt, gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition, pSoldier->ubID)); DebugAI(AI_MSG_INFO, pSoldier, String("current team %d interrupt occurred %d", gTacticalStatus.ubCurrentTeam, gTacticalStatus.fInterruptOccurred)); DebugAI(AI_MSG_INFO, pSoldier, String("AP=%d/%d %s %s %s %s %s", pSoldier->bActionPoints, pSoldier->bInitialActionPoints, gStr8AlertStatus[pSoldier->aiData.bAlertStatus], gStr8Orders[pSoldier->aiData.bOrders], gStr8Attitude[pSoldier->aiData.bAttitude], gStr8Team[pSoldier->bTeam], gStr8Class[pSoldier->ubSoldierClass])); DebugAI(AI_MSG_INFO, pSoldier, String("Health %d/%d Breath %d/%d Shock %d Tolerance %d AI Morale %d Morale %d", pSoldier->stats.bLife, pSoldier->stats.bLifeMax, pSoldier->bBreath, pSoldier->bBreathMax, pSoldier->aiData.bShock, CalcSuppressionTolerance(pSoldier), pSoldier->aiData.bAIMorale, pSoldier->aiData.bMorale)); From 664142c36c187597e0d0aab61edd9f3c91b3c475 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 17 Aug 2023 19:50:59 +0300 Subject: [PATCH 07/80] Don't log AI info if pSoldier is null --- TacticalAI/AIMain.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 6aae43e67..74004fcf5 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -307,16 +307,19 @@ void DebugAI( INT8 bMsgType, SOLDIERTYPE *pSoldier, STR szOutput, INT8 bAction ) } // also log to individual file for selected soldier - sprintf(buf, "Logs\\AI_Decisions [%d].txt", pSoldier->ubID); - if ((DebugFile = fopen(buf, "a+t")) != NULL) + if (pSoldier) { - if (bMsgType == AI_MSG_START) + sprintf(buf, "Logs\\AI_Decisions [%d].txt", pSoldier->ubID); + if ((DebugFile = fopen(buf, "a+t")) != NULL) { + if (bMsgType == AI_MSG_START) + { + fputs("\n", DebugFile); + } + fputs(msg, DebugFile); fputs("\n", DebugFile); + fclose(DebugFile); } - fputs(msg, DebugFile); - fputs("\n", DebugFile); - fclose(DebugFile); } } From 1c4929c3e1e85d62bec7a4a39b2f7eea823d8999 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 17 Aug 2023 19:52:21 +0300 Subject: [PATCH 08/80] Improve AI item handling log entry --- TacticalAI/AIMain.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 74004fcf5..fe20bd12b 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -2317,10 +2317,9 @@ INT8 ExecuteAction(SOLDIERTYPE *pSoldier) { if ( iRetCode != ITEM_HANDLE_BROKEN ) // if the item broke, this is 'legal' and doesn't need reporting { - DebugAI( String( "AI %d got error code %ld from HandleItem, doing action %d, has %d APs... aborting deadlock!", pSoldier->ubID, iRetCode, pSoldier->aiData.bAction, pSoldier->bActionPoints ) ); + DebugAI(AI_MSG_INFO, pSoldier, String( "AI %d got error code %ld from HandleItem, doing action %d, has %d APs... aborting deadlock!", pSoldier->ubID, iRetCode, pSoldier->aiData.bAction, pSoldier->bActionPoints ) ); ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d got error code %ld from HandleItem, doing action %d... aborting deadlock!", pSoldier->ubID, iRetCode, pSoldier->aiData.bAction ); } - DebugAI(AI_MSG_INFO, pSoldier, String("CancelAIAction: !ITEM_HANDLE_OK")); CancelAIAction( pSoldier, FORCE); #ifdef TESTAICONTROL if (gfTurnBasedAI) From 613de1b8691c3162976644ddca61ba8c6bbb40a5 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Fri, 18 Aug 2023 21:05:11 +0300 Subject: [PATCH 09/80] Remove useless code !AimingGun(pSoldier) would always evaluate to true and there are no hits to CrowDecideAction in the entire solution even in commented out code --- TacticalAI/AIInternals.h | 2 -- TacticalAI/AIMain.cpp | 4 ---- TacticalAI/DecideAction.cpp | 6 +++--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/TacticalAI/AIInternals.h b/TacticalAI/AIInternals.h index 1e72dedc8..56881bad7 100644 --- a/TacticalAI/AIInternals.h +++ b/TacticalAI/AIInternals.h @@ -190,7 +190,6 @@ typedef enum INT16 AdvanceToFiringRange( SOLDIERTYPE * pSoldier, INT16 sClosestOpponent ); -BOOLEAN AimingGun(SOLDIERTYPE *pSoldier); void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot); void CalcBestStab(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestStab, BOOLEAN fBladeAttack); void CalcBestThrow(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestThrow); @@ -223,7 +222,6 @@ INT8 ArmedVehicleDecideAction( SOLDIERTYPE* pSoldier ); // a variant of ClosestSeenOpponent(...), that allows to find enemies on a roof INT32 ClosestSeenOpponentWithRoof(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel); -INT8 CrowDecideAction( SOLDIERTYPE * pSoldier ); void DecideAlertStatus( SOLDIERTYPE *pSoldier ); INT8 DecideAutoBandage( SOLDIERTYPE * pSoldier ); UINT16 DetermineMovementMode( SOLDIERTYPE * pSoldier, INT8 bAction ); diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index fe20bd12b..b64067a88 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -408,10 +408,6 @@ BOOLEAN InitAI( void ) return( TRUE ); } -BOOLEAN AimingGun(SOLDIERTYPE *pSoldier) -{ - return(FALSE); -} void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named inappropriately { diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 9e5ea8524..3967c2812 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -7086,7 +7086,7 @@ INT16 ubMinAPCost; { // we might want to turn before lying down! if ( (!gfTurnBasedAI || GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints - GetAPsToChangeStance( pSoldier, (INT8) pSoldier->aiData.usActionData )) && - (((pSoldier->aiData.bAIMorale > MORALE_HOPELESS) || ubCanMove) && !AimingGun(pSoldier)) ) + ((pSoldier->aiData.bAIMorale > MORALE_HOPELESS) || ubCanMove) ) { // if we have a closest seen opponent if (!TileIsOutOfBounds(sClosestOpponent)) @@ -7140,7 +7140,7 @@ INT16 ubMinAPCost; // hopeless guys shouldn't waste their time this way, UNLESS they CAN move // but chose not to to get this far (which probably means they're cornered) // ALSO, don't bother turning if we're already aiming a gun - if ( !gfHiddenInterrupt && ((pSoldier->aiData.bAIMorale > MORALE_HOPELESS) || ubCanMove) && !AimingGun(pSoldier)) + if ( !gfHiddenInterrupt && ((pSoldier->aiData.bAIMorale > MORALE_HOPELESS) || ubCanMove) ) { // determine the location of the known closest opponent // (don't care if he's conscious, don't care if he's reachable at all) @@ -10297,7 +10297,7 @@ INT8 ArmedVehicleDecideActionBlack( SOLDIERTYPE *pSoldier ) // hopeless guys shouldn't waste their time this way, UNLESS they CAN move // but chose not to to get this far (which probably means they're cornered) // ALSO, don't bother turning if we're already aiming a gun - if ( !gfHiddenInterrupt && ((pSoldier->aiData.bAIMorale > MORALE_HOPELESS) || ubCanMove) && !AimingGun( pSoldier ) ) + if ( !gfHiddenInterrupt && ((pSoldier->aiData.bAIMorale > MORALE_HOPELESS) || ubCanMove) ) { // determine the location of the known closest opponent // (don't care if he's conscious, don't care if he's reachable at all) From 06478266d0fea43f1d875a8a1e6c4848ccdc96ac Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Fri, 18 Aug 2023 23:11:39 +0300 Subject: [PATCH 10/80] Moved armed vehicle/robot in gas check into function Originally placed in DecideAction(green/yellow/red/black) functions separately --- TacticalAI/AIUtils.cpp | 12 ++++++++++++ TacticalAI/DecideAction.cpp | 18 ------------------ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/TacticalAI/AIUtils.cpp b/TacticalAI/AIUtils.cpp index 2f947a9fe..4a6f1c29f 100644 --- a/TacticalAI/AIUtils.cpp +++ b/TacticalAI/AIUtils.cpp @@ -2253,6 +2253,12 @@ BOOLEAN InWaterGasOrSmoke( SOLDIERTYPE *pSoldier, INT32 sGridNo ) BOOLEAN InGasOrSmoke( SOLDIERTYPE *pSoldier, INT32 sGridNo ) { + // Armed vehicles and robots do not care about gas or smoke + if (ARMED_VEHICLE(pSoldier) || ENEMYROBOT(pSoldier)) + { + return FALSE; + } + // smoke if ( gpWorldLevelData[sGridNo].ubExtFlags[pSoldier->pathing.bLevel] & (MAPELEMENT_EXT_SMOKE | MAPELEMENT_EXT_SIGNAL_SMOKE | MAPELEMENT_EXT_DEBRIS_SMOKE | MAPELEMENT_EXT_FIRERETARDANT_SMOKE ) ) return TRUE; @@ -2300,6 +2306,12 @@ BOOLEAN InGas(SOLDIERTYPE *pSoldier, INT32 sGridNo) if (TileIsOutOfBounds(sGridNo)) return FALSE; + // Armed vehicles and robots do not care about gas or smoke + if (ARMED_VEHICLE(pSoldier) || ENEMYROBOT(pSoldier)) + { + return FALSE; + } + if (InGasSpot(pSoldier, sGridNo, pSoldier->pathing.bLevel)) { return TRUE; diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 3967c2812..ed30f0369 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -807,12 +807,6 @@ INT8 DecideActionGreen(SOLDIERTYPE *pSoldier) // check if standing in tear gas without a gas mask on, or in smoke bInGas = InGasOrSmoke( pSoldier, pSoldier->sGridNo ); - // Flugente: tanks do not care about gas - if ( ARMED_VEHICLE( pSoldier ) || ENEMYROBOT( pSoldier ) ) - { - bInGas = FALSE; - } - // if real-time, and not in the way, do nothing 90% of the time (for GUARDS!) // unless in water (could've started there), then we better swim to shore! @@ -2589,12 +2583,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // check if standing in tear gas without a gas mask on bInGas = InGasOrSmoke( pSoldier, pSoldier->sGridNo ); - // Flugente: tanks do not care about gas - if ( ARMED_VEHICLE( pSoldier ) || ENEMYROBOT( pSoldier ) ) - { - bInGas = FALSE; - } - //////////////////////////////////////////////////////////////////////////// // WHEN LEFT IN GAS, WEAR GAS MASK IF AVAILABLE AND NOT WORN //////////////////////////////////////////////////////////////////////////// @@ -5042,12 +5030,6 @@ INT16 ubMinAPCost; // check if standing in tear gas without a gas mask on bInGas = InGasOrSmoke( pSoldier, pSoldier->sGridNo ); - // Flugente: tanks do not care about gas - if ( ARMED_VEHICLE( pSoldier ) || ENEMYROBOT( pSoldier ) ) - { - bInGas = FALSE; - } - // calculate our morale pSoldier->aiData.bAIMorale = CalcMorale(pSoldier); From a0cbe42a01ed2c4eddad5409aa8336442d4d6441 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Fri, 18 Aug 2023 23:56:40 +0300 Subject: [PATCH 11/80] Create function for decision to wear a gasmask --- TacticalAI/DecideAction.cpp | 64 ++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index ed30f0369..b20ce2dc2 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -46,6 +46,7 @@ extern UINT16 PickSoldierReadyAnimation( SOLDIERTYPE *pSoldier, BOOLEAN fEndRead extern void IncrementWatchedLoc(UINT8 ubID, INT32 sGridNo, INT8 bLevel); void LogDecideInfo(SOLDIERTYPE *pSoldier); void LogKnowledgeInfo(SOLDIERTYPE *pSoldier); +INT8 DecideActionWearGasmask(SOLDIERTYPE* pSoldier); // global status time counters to determine what takes the most time @@ -2450,7 +2451,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) INT32 sClosestDisturbance = NOWHERE, sCheckGridNo; INT32 sDistVisible; UINT8 ubCanMove,ubOpponentDir; - INT8 bInWater, bInDeepWater, bInGas; INT8 bSeekPts = 0, bHelpPts = 0, bHidePts = 0, bWatchPts = 0; INT8 bHighestWatchLoc; ATTACKTYPE BestThrow, BestShot; @@ -2577,29 +2577,13 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // determine if we happen to be in water (in which case we're in BIG trouble!) - bInWater = Water( pSoldier->sGridNo, pSoldier->pathing.bLevel ); - bInDeepWater = DeepWater( pSoldier->sGridNo, pSoldier->pathing.bLevel ); - - // check if standing in tear gas without a gas mask on - bInGas = InGasOrSmoke( pSoldier, pSoldier->sGridNo ); + INT8 bInWater = Water( pSoldier->sGridNo, pSoldier->pathing.bLevel ); + INT8 bInDeepWater = DeepWater( pSoldier->sGridNo, pSoldier->pathing.bLevel ); //////////////////////////////////////////////////////////////////////////// // WHEN LEFT IN GAS, WEAR GAS MASK IF AVAILABLE AND NOT WORN //////////////////////////////////////////////////////////////////////////// - - if ( !bInGas && (gWorldSectorX == TIXA_SECTOR_X && gWorldSectorY == TIXA_SECTOR_Y) ) - { - // only chance if we happen to be caught with our gas mask off - if ( PreRandom( 10 ) == 0 && WearGasMaskIfAvailable( pSoldier ) ) - { - // reevaluate - bInGas = InGasOrSmoke( pSoldier, pSoldier->sGridNo ); - } - } - - //Only put mask on in gas - if(bInGas && WearGasMaskIfAvailable(pSoldier))//dnl ch40 200909 - bInGas = InGasOrSmoke(pSoldier, pSoldier->sGridNo); + INT8 bInGas = DecideActionWearGasmask(pSoldier); //////////////////////////////////////////////////////////////////////////// // WHEN IN GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND @@ -4878,7 +4862,6 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) INT32 sClosestDisturbance; INT16 ubMinAPCost; UINT8 ubCanMove; - INT8 bInWater,bInDeepWater,bInGas; INT8 bDirection; UINT8 ubBestAttackAction = AI_ACTION_NONE; INT8 bCanAttack,bActionReturned; @@ -4998,6 +4981,7 @@ INT16 ubMinAPCost; } } + INT8 bInWater, bInDeepWater, bInGas; if ( pSoldier->flags.uiStatusFlags & SOLDIER_BOXER ) { if ( gTacticalStatus.bBoxingState == PRE_BOXING ) @@ -5027,28 +5011,13 @@ INT16 ubMinAPCost; bInWater = Water( pSoldier->sGridNo, pSoldier->pathing.bLevel ); bInDeepWater = WaterTooDeepForAttacks( pSoldier->sGridNo, pSoldier->pathing.bLevel ); - // check if standing in tear gas without a gas mask on - bInGas = InGasOrSmoke( pSoldier, pSoldier->sGridNo ); - // calculate our morale pSoldier->aiData.bAIMorale = CalcMorale(pSoldier); //////////////////////////////////////////////////////////////////////////// // WHEN LEFT IN GAS, WEAR GAS MASK IF AVAILABLE AND NOT WORN //////////////////////////////////////////////////////////////////////////// - - if ( !bInGas && (gWorldSectorX == TIXA_SECTOR_X && gWorldSectorY == TIXA_SECTOR_Y) ) - { - // only chance if we happen to be caught with our gas mask off - if ( PreRandom( 10 ) == 0 && WearGasMaskIfAvailable( pSoldier ) ) - { - bInGas = FALSE; - } - } - - //Only put mask on in gas - if(bInGas && WearGasMaskIfAvailable(pSoldier))//dnl ch40 200909 - bInGas = InGasOrSmoke(pSoldier, pSoldier->sGridNo); + bInGas = DecideActionWearGasmask(pSoldier); //////////////////////////////////////////////////////////////////////////// // IF GASSED, OR REALLY TIRED (ON THE VERGE OF COLLAPSING), TRY TO RUN AWAY @@ -10392,3 +10361,24 @@ void LogKnowledgeInfo(SOLDIERTYPE *pSoldier) } } } + + +INT8 DecideActionWearGasmask(SOLDIERTYPE *pSoldier) +{ + // check if standing in tear gas without a gas mask on + INT8 bInGas = InGasOrSmoke(pSoldier, pSoldier->sGridNo); + + if (!bInGas && (gWorldSectorX == TIXA_SECTOR_X && gWorldSectorY == TIXA_SECTOR_Y)) + { + // only chance if we happen to be caught with our gas mask off + if (PreRandom(10) == 0 && WearGasMaskIfAvailable(pSoldier)) + { + bInGas = FALSE; + } + } + + //Only put mask on in gas + if (bInGas && WearGasMaskIfAvailable(pSoldier)) { bInGas = InGasOrSmoke(pSoldier, pSoldier->sGridNo); } + + return bInGas; +} From 138ad0656fa21a1004e57510818f7e86b20c1ec8 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 19 Aug 2023 14:37:49 +0300 Subject: [PATCH 12/80] Prevent AI deadlocks Canceling current AI actions at the start of soldier's turn prevents a lot of ai deadlocks. The status RED & BLACK actions are supposed re-evaluate the situation every turn anyways --- TacticalAI/AIMain.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index b64067a88..b2eca435a 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -1817,6 +1817,7 @@ void RefreshAI(SOLDIERTYPE *pSoldier) if ((pSoldier->aiData.bAlertStatus == STATUS_BLACK) || (pSoldier->aiData.bAlertStatus == STATUS_RED)) { // always freshly rethink things at start of his turn + CancelAIAction(pSoldier, FALSE); pSoldier->aiData.bNewSituation = IS_NEW_SITUATION; } else From c5d1615035bd0d25c1bdb3b98eaeb784c50492fd Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 19 Aug 2023 14:39:33 +0300 Subject: [PATCH 13/80] Move decision if stuck in water or gas into its own function --- TacticalAI/DecideAction.cpp | 128 ++++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 57 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index b20ce2dc2..d94c24650 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -47,6 +47,7 @@ extern void IncrementWatchedLoc(UINT8 ubID, INT32 sGridNo, INT8 bLevel); void LogDecideInfo(SOLDIERTYPE *pSoldier); void LogKnowledgeInfo(SOLDIERTYPE *pSoldier); INT8 DecideActionWearGasmask(SOLDIERTYPE* pSoldier); +ActionType DecideActionStuckInWaterOrGas(SOLDIERTYPE* pSoldier, BOOLEAN ubCanMove, BOOLEAN bInWater, BOOLEAN bInDeepWater, BOOLEAN bInGas); // global status time counters to determine what takes the most time @@ -4401,7 +4402,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { // look for best place to RUN AWAY to (farthest from the closest threat) pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); - + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { #ifdef DEBUGDECISIONS @@ -4861,7 +4862,6 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) INT32 sClosestOpponent = NOWHERE,sBestCover = NOWHERE;//dnl ch58 160813 INT32 sClosestDisturbance; INT16 ubMinAPCost; - UINT8 ubCanMove; INT8 bDirection; UINT8 ubBestAttackAction = AI_ACTION_NONE; INT8 bCanAttack,bActionReturned; @@ -4877,7 +4877,6 @@ INT16 ubMinAPCost; ATTACKTYPE BestShot, BestThrow, BestStab ,BestAttack;//dnl ch69 150913 BOOLEAN fCivilian = (PTR_CIVILIAN && (pSoldier->ubCivilianGroup == NON_CIV_GROUP || pSoldier->aiData.bNeutral || (pSoldier->ubBodyType >= FATCIV && pSoldier->ubBodyType <= CRIPPLECIV) ) ); - BOOLEAN fClimb; INT16 ubBurstAPs; UINT8 ubOpponentDir; INT32 sCheckGridNo; @@ -4912,7 +4911,7 @@ INT16 ubMinAPCost; } // can this guy move to any of the neighbouring squares ? (sets TRUE/FALSE) - ubCanMove = (pSoldier->bActionPoints >= MinPtsToMove(pSoldier)); + UINT8 ubCanMove = (pSoldier->bActionPoints >= MinPtsToMove(pSoldier)); if( pSoldier->flags.uiStatusFlags & ( SOLDIER_DRIVER | SOLDIER_PASSENGER ) ) { @@ -5019,6 +5018,7 @@ INT16 ubMinAPCost; //////////////////////////////////////////////////////////////////////////// bInGas = DecideActionWearGasmask(pSoldier); + //////////////////////////////////////////////////////////////////////////// // IF GASSED, OR REALLY TIRED (ON THE VERGE OF COLLAPSING), TRY TO RUN AWAY //////////////////////////////////////////////////////////////////////////// @@ -5060,58 +5060,10 @@ INT16 ubMinAPCost; //////////////////////////////////////////////////////////////////////////// // STUCK IN WATER OR GAS, NO COVER, GO TO NEAREST SPOT OF UNGASSED LAND //////////////////////////////////////////////////////////////////////////// - - // when in deep water, move to closest opponent - if (ubCanMove && bInDeepWater && !pSoldier->aiData.bNeutral && pSoldier->aiData.bOrders == SEEKENEMY) + auto decision = DecideActionStuckInWaterOrGas(pSoldier, ubCanMove, bInWater, bInDeepWater, bInGas); + if (decision != AI_ACTION_LAST) { - // find closest reachable opponent, excluding opponents in deep water - pSoldier->aiData.usActionData = ClosestReachableDisturbance(pSoldier, &fClimb); - - if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) - { - return(AI_ACTION_LEAVE_WATER_GAS); - } - } - - // if soldier in water/gas has enough APs left to move at least 1 square - if (ubCanMove && (bInGas || bInDeepWater || FindBombNearby(pSoldier, pSoldier->sGridNo, BOMB_DETECTION_RANGE) || RedSmokeDanger(pSoldier->sGridNo, pSoldier->pathing.bLevel))) - { - pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier); - - if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) - { -#ifdef DEBUGDECISIONS - sprintf(tempstr,"%s - SEEKING NEAREST UNGASSED LAND at grid %d",pSoldier->name,pSoldier->aiData.usActionData); - AIPopMessage(tempstr); -#endif - - return(AI_ACTION_LEAVE_WATER_GAS); - } - - // couldn't find ANY land within 25 tiles(!), this should never happen... - - // look for best place to RUN AWAY to (farthest from the closest threat) - pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); - - if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) - { -#ifdef DEBUGDECISIONS - sprintf(tempstr,"%s - NO LAND NEAR, RUNNING AWAY to grid %d",pSoldier->name,pSoldier->aiData.usActionData); - AIPopMessage(tempstr); -#endif - - return(AI_ACTION_RUN_AWAY); - } - - // GIVE UP ON LIFE! MERCS MUST HAVE JUST CORNERED A HELPLESS ENEMY IN A - // GAS FILLED ROOM (OR IN WATER MORE THAN 25 TILES FROM NEAREST LAND...) - if ( bInGas && gGameOptions.ubDifficultyLevel == DIF_LEVEL_INSANE ) - { - pSoldier->bBreath = pSoldier->bBreathMax; - pSoldier->aiData.bAIMorale = MORALE_FEARLESS; // Can't move, can't get away, go nuts instead... - } - else - pSoldier->aiData.bAIMorale = MORALE_HOPELESS; + return decision; } // offer surrender? @@ -5432,7 +5384,8 @@ INT16 ubMinAPCost; (pSoldier->aiData.bAttitude != AGGRESSIVE || Chance((100 - BestShot.ubChanceToReallyHit) / 2))) { // get the location of the closest CONSCIOUS reachable opponent - sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb); + BOOLEAN fClimbDummy; + sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimbDummy); // if we found one if (!TileIsOutOfBounds(sClosestDisturbance)) @@ -5950,7 +5903,8 @@ INT16 ubMinAPCost; DebugAI(AI_MSG_TOPIC, pSoldier, String("[Black cover advance]")); DebugAI(AI_MSG_INFO, pSoldier, String("find cover advance spot")); - INT32 sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb); + BOOLEAN fClimbDummy; + INT32 sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimbDummy); if (!TileIsOutOfBounds(sClosestDisturbance)) { @@ -10382,3 +10336,63 @@ INT8 DecideActionWearGasmask(SOLDIERTYPE *pSoldier) return bInGas; } + +ActionType DecideActionStuckInWaterOrGas(SOLDIERTYPE *pSoldier, BOOLEAN ubCanMove, BOOLEAN bInWater, BOOLEAN bInDeepWater, BOOLEAN bInGas) +{ + // when in deep water, move to closest opponent + if (ubCanMove && (bInDeepWater || bInWater) && !pSoldier->aiData.bNeutral && pSoldier->aiData.bOrders == SEEKENEMY) + { + // find closest reachable opponent, excluding opponents in deep water + BOOLEAN fClimb; + pSoldier->aiData.usActionData = ClosestReachableDisturbance(pSoldier, &fClimb); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_LEAVE_WATER_GAS); + } + } + + // if soldier in water/gas has enough APs left to move at least 1 square + if (ubCanMove && (bInGas || bInWater || bInDeepWater || FindBombNearby(pSoldier, pSoldier->sGridNo, BOMB_DETECTION_RANGE) || RedSmokeDanger(pSoldier->sGridNo, pSoldier->pathing.bLevel))) + { + pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - SEEKING NEAREST UNGASSED LAND at grid %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + return(AI_ACTION_LEAVE_WATER_GAS); + } + + // couldn't find ANY land within 25 tiles(!), this should never happen... + + // look for best place to RUN AWAY to (farthest from the closest threat) + pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - NO LAND NEAR, RUNNING AWAY to grid %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + return(AI_ACTION_RUN_AWAY); + } + + // GIVE UP ON LIFE! MERCS MUST HAVE JUST CORNERED A HELPLESS ENEMY IN A + // GAS FILLED ROOM (OR IN WATER MORE THAN 25 TILES FROM NEAREST LAND...) + if ((bInGas || bInWater || bInDeepWater) && gGameOptions.ubDifficultyLevel == DIF_LEVEL_INSANE) + { + pSoldier->bBreath = pSoldier->bBreathMax; + pSoldier->aiData.bAIMorale = MORALE_FEARLESS; // Can't move, can't get away, go nuts instead... + } + else + pSoldier->aiData.bAIMorale = MORALE_HOPELESS; + } + + + return AI_ACTION_LAST; +} From 96a7fa0538aa3bf0e15f4ac3df82e598b6c73a24 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 19 Aug 2023 14:41:16 +0300 Subject: [PATCH 14/80] Add section comments to DecideActionBlack --- TacticalAI/DecideAction.cpp | 60 +++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index d94c24650..8afefe6c5 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -5196,6 +5196,9 @@ INT16 ubMinAPCost; } } + //////////////////////////////////////////////////////////////////////////// + // THROW A SMOKE GRENADE FOR COVER + //////////////////////////////////////////////////////////////////////////// if (SoldierAI(pSoldier) && gfTurnBasedAI && pSoldier->bActionPoints == pSoldier->bInitialActionPoints && @@ -5250,6 +5253,11 @@ INT16 ubMinAPCost; } } + + //////////////////////////////////////////////////////////////////////////// + // LOOK FOR A WEAPON + //////////////////////////////////////////////////////////////////////////// + // if we don't have a gun, look around for a weapon! if (FindAIUsableObjClass( pSoldier, IC_GUN ) == ITEM_NOT_FOUND && ubCanMove && !pSoldier->aiData.bNeutral) { @@ -5261,8 +5269,10 @@ INT16 ubMinAPCost; } } - // Flugente: trait skills - // if we are a radio operator + + //////////////////////////////////////////////////////////////////////////// + // RADIO OPERATOR TRAIT + //////////////////////////////////////////////////////////////////////////// if ( HAS_SKILL_TRAIT( pSoldier, RADIO_OPERATOR_NT ) > 0 && pSoldier->CanUseSkill(SKILLS_RADIO_ARTILLERY, TRUE) ) { // check: would it be possible to call in artillery from neighbouring sectors? @@ -5320,6 +5330,11 @@ INT16 ubMinAPCost; } } + + + //////////////////////////////////////////////////////////////////////////// + // VIP RETREAT + //////////////////////////////////////////////////////////////////////////// // VIPs run away (but not the GENERAL) if ( pSoldier->usSoldierFlagMask & SOLDIER_VIP && pSoldier->ubProfile != GENERAL ) { @@ -5339,6 +5354,10 @@ INT16 ubMinAPCost; } } + + //////////////////////////////////////////////////////////////////////////// + // DETERMINE BEST ATTACK + //////////////////////////////////////////////////////////////////////////// BestShot.ubPossible = FALSE; // by default, assume Shooting isn't possible BestThrow.ubPossible = FALSE; // by default, assume Throwing isn't possible BestStab.ubPossible = FALSE; // by default, assume Stabbing isn't possible @@ -5846,6 +5865,10 @@ INT16 ubMinAPCost; UINT16 usRange = BestAttack.bWeaponIn==NO_SLOT ? 0 : GetModifiedGunRange(pSoldier->inv[BestAttack.bWeaponIn].usItem);//dnl ch69 150913 INT32 sClosestThreat = ClosestKnownOpponent(pSoldier, NULL, NULL); + + ////////////////////////////////////////////////////////////////////////// + // STATUS BLACK RETREAT + ////////////////////////////////////////////////////////////////////////// if (gfTurnBasedAI && !bInWater && ubCanMove && @@ -5873,7 +5896,10 @@ INT16 ubMinAPCost; } } - // Black cover advance + + ////////////////////////////////////////////////////////////////////////// + // STATUS BLACK ADVANCE TO COVER + ////////////////////////////////////////////////////////////////////////// if (SoldierAI(pSoldier) && gfTurnBasedAI && //!pSoldier->bActionPoints == pSoldier->bInitialActionPoints && @@ -6031,6 +6057,10 @@ INT16 ubMinAPCost; } } + + //////////////////////////////////////////////////////////////////////////// + // POSSIBLY FORGET THE ATTACK AND TAKE COVER + //////////////////////////////////////////////////////////////////////////// if ( (pSoldier->bActionPoints == pSoldier->bInitialActionPoints) && (ubBestAttackAction == AI_ACTION_FIRE_GUN) && (pSoldier->aiData.bShock == 0) && @@ -6065,7 +6095,6 @@ INT16 ubMinAPCost; } } } - } DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"LOOK FOR SOME KIND OF COVER BETTER THAN WHAT WE HAVE NOW"); @@ -6174,6 +6203,10 @@ INT16 ubMinAPCost; } } + + ////////////////////////////////////////////////////////////////////////// + // PREPARE ATTACK + ////////////////////////////////////////////////////////////////////////// DebugMsg (TOPIC_JA2,DBG_LEVEL_3,String("DecideActionBlack: is attack still desirable? ubBestAttackAction = %d",ubBestAttackAction)); // if attack is still desirable (meaning it's also preferred to taking cover) @@ -6647,7 +6680,10 @@ INT16 ubMinAPCost; return( AI_ACTION_NONE ); } - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Window jump]")); + + ////////////////////////////////////////////////////////////////////// + // CLIMB ROOF / JUMP THROUGH WINDOW + ////////////////////////////////////////////////////////////////////// // get the location of the closest reachable opponent /* Flugente 22.02.2012 - A few clarifications: I changed ClosestSeenOpponent so that for zombies, this function also returns an opponent if he is on the * roof of a building, we are not, but our GridNo belongs to that same building. @@ -6660,6 +6696,7 @@ INT16 ubMinAPCost; sClosestOpponent = ClosestSeenOpponentWithRoof(pSoldier, &targetGridNo, &targetbLevel); if ( !TileIsOutOfBounds(sClosestOpponent) && !TileIsOutOfBounds(targetGridNo) && SameBuilding( pSoldier->sGridNo, targetGridNo ) ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Window jump]")); if ( targetbLevel == pSoldier->pathing.bLevel && targetbLevel == 0 ) { ////////////////////////////////////////////////////////////////////// @@ -6736,7 +6773,10 @@ INT16 ubMinAPCost; } } - // try to make boxer close if possible + + ////////////////////////////////////////////////////////////////////// + // BOXER CLOSE IN ON OPPONENT + ////////////////////////////////////////////////////////////////////// if (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER ) { DebugAI(AI_MSG_TOPIC, pSoldier, String("[Make boxer close if possible]")); @@ -6882,10 +6922,10 @@ INT16 ubMinAPCost; return(AI_ACTION_NONE); } + //////////////////////////////////////////////////////////////////////////// // IF A LOCATION WITH BETTER COVER IS AVAILABLE & REACHABLE, GO FOR IT! //////////////////////////////////////////////////////////////////////////// - if (!TileIsOutOfBounds(sBestCover)) { DebugAI(AI_MSG_TOPIC, pSoldier, String("[Take cover]")); @@ -6906,7 +6946,8 @@ INT16 ubMinAPCost; } return(AI_ACTION_TAKE_COVER); } - + + //////////////////////////////////////////////////////////////////////////// // IF THINGS ARE REALLY HOPELESS, OR UNARMED, TRY TO RUN AWAY //////////////////////////////////////////////////////////////////////////// @@ -6933,6 +6974,7 @@ INT16 ubMinAPCost; } } + //////////////////////////////////////////////////////////////////////////// // IF SPOTTERS HAVE BEEN CALLED FOR, AND WE HAVE SOME NEW SIGHTINGS, RADIO! //////////////////////////////////////////////////////////////////////////// @@ -7036,6 +7078,7 @@ INT16 ubMinAPCost; } } + //////////////////////////////////////////////////////////////////////////// // TURN TO FACE CLOSEST KNOWN OPPONENT (IF NOT FACING THERE ALREADY) //////////////////////////////////////////////////////////////////////////// @@ -7176,7 +7219,6 @@ INT16 ubMinAPCost; // by default, if everything else fails, just stand in place and wait pSoldier->aiData.usActionData = NOWHERE; return(AI_ACTION_NONE); - } void DecideAlertStatus( SOLDIERTYPE *pSoldier ) From d28ed97dffa8ee9f938f6397724ab94735046b2e Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 19 Aug 2023 21:45:01 +0300 Subject: [PATCH 15/80] Add section comments do DecideActionRed --- TacticalAI/DecideAction.cpp | 52 ++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 8afefe6c5..fe1f109df 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -2617,6 +2617,10 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } + + //////////////////////////////////////////////////////////////////////////// + // REGULAR CIVILIANS COWER / RUN AWAY + //////////////////////////////////////////////////////////////////////////// //if (fCivilian && !(pSoldier->ubBodyType == COW || pSoldier->ubBodyType == CRIPPLECIV || pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE) && gTacticalStatus.bBoxingState == NOT_BOXING) if (fCivilian && !(pSoldier->ubBodyType == COW || pSoldier->ubBodyType == CRIPPLECIV || pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE)) { @@ -2684,6 +2688,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } + //////////////////////////////////////////////////////////////////////// // IF POSSIBLE, FIRE LONG RANGE WEAPONS AT TARGETS REPORTED BY RADIO //////////////////////////////////////////////////////////////////////// @@ -2703,6 +2708,9 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) CheckIfTossPossible(pSoldier,&BestThrow); + //////////////////////////////////////////////////////////////////////// + // CHECK IF THROWING A GRENADE OR USING A LAUNCHER/MORTAR AGAINST ENEMY IS POSSIBLE + //////////////////////////////////////////////////////////////////////// if (BestThrow.ubPossible) { DebugAI(AI_MSG_INFO, pSoldier, String("throw possible")); @@ -2830,7 +2838,10 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } - // use smoke to cover friend + + //////////////////////////////////////////////////////////////////////// + // THROW SMOKE TO PROVIDE COVER FOR FRIEND + //////////////////////////////////////////////////////////////////////// if (gfTurnBasedAI && SoldierAI(pSoldier) && !bInWater && @@ -2892,6 +2903,10 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } + + //////////////////////////////////////////////////////////////////////// + // SNIPER / SUPPRESSION + //////////////////////////////////////////////////////////////////////// // sevenfm: moved can attack check here as only sniper/suppression code needs usable gun if(CanNPCAttack(pSoldier) == TRUE) { @@ -3146,8 +3161,10 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } // suppression not possible, do something else - // Flugente: trait skills - // if we are a radio operator + + //////////////////////////////////////////////////////////////////////// + // RADIO OPERATOR + //////////////////////////////////////////////////////////////////////// if (HAS_SKILL_TRAIT(pSoldier, RADIO_OPERATOR_NT) > 0 && pSoldier->CanUseSkill(SKILLS_RADIO_ARTILLERY, TRUE)) { @@ -3289,6 +3306,11 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } + + //////////////////////////////////////////////////////////////////////////// + // PROVIDE / SEEK MEDICAL AID + //////////////////////////////////////////////////////////////////////////// + // if we are a doctor with medical gear, we might be able to help a wounded ally if ( pSoldier->CanMedicAI() ) { @@ -3362,6 +3384,10 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } + + //////////////////////////////////////////////////////////////////////////// + // VIP RETREAT + //////////////////////////////////////////////////////////////////////////// // VIPs run away (but not the GENERAL) if ( pSoldier->usSoldierFlagMask & SOLDIER_VIP && pSoldier->ubProfile != GENERAL ) { @@ -3381,6 +3407,10 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } + + //////////////////////////////////////////////////////////////////////////// + // PROTECT VIP + //////////////////////////////////////////////////////////////////////////// // are we a bodyguard? if ( pSoldier->usSoldierFlagMask & SOLDIER_BODYGUARD ) { @@ -3403,6 +3433,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } + //////////////////////////////////////////////////////////////////////// // RED RETREAT //////////////////////////////////////////////////////////////////////// @@ -3431,10 +3462,11 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } - DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: crouch and rest if running out of breath"); + //////////////////////////////////////////////////////////////////////// // CROUCH & REST IF RUNNING OUT OF BREATH //////////////////////////////////////////////////////////////////////// + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: crouch and rest if running out of breath"); // if our breath is running a bit low, and we're not in water or under fire if ((pSoldier->bBreath < 25) && !bInWater && !pSoldier->aiData.bUnderFire) @@ -3501,7 +3533,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } - DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: radio red alert?"); //////////////////////////////////////////////////////////////////////////// // RADIO RED ALERT: determine %chance to call others and report contact //////////////////////////////////////////////////////////////////////////// @@ -3510,7 +3541,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // (we never want NPCs to choose to radio if they would have to wait a turn) if ( !(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT) && !fCivilian && (pSoldier->bActionPoints >= APBPConstants[AP_RADIO]) && (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1) ) { - DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: checking to radio red alert"); // if there hasn't been an initial RED ALERT yet in this sector @@ -3583,6 +3613,10 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } + + //////////////////////////////////////////////////////////////////////////// + // THROW A SMOKE GRENADE FOR COVER + //////////////////////////////////////////////////////////////////////////// if (gfTurnBasedAI && pSoldier->bActionPoints == pSoldier->bInitialActionPoints && pSoldier->aiData.bUnderFire && @@ -3636,6 +3670,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } + // sevenfm: no Main Red AI for civilians if ( (gGameExternalOptions.fEnemyTanksCanMoveInTactical || !ARMED_VEHICLE( pSoldier )) && !(pSoldier->flags.uiStatusFlags & (SOLDIER_DRIVER | SOLDIER_PASSENGER)) && @@ -3643,7 +3678,10 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: main red ai"); - // sevenfm: avoid light if spot is dangerous and no friends see my closest enemy + + //////////////////////////////////////////////////////////////////////////// + // AVOID LIGHT IF SPOT IS DANGEROUS AND NO FRIENDS SEE MY CLOSEST ENEMY + //////////////////////////////////////////////////////////////////////////// if (ubCanMove && InLightAtNight( pSoldier->sGridNo, pSoldier->pathing.bLevel ) && pSoldier->aiData.bOrders != STATIONARY && From 1170f8215994bbb45fbe32f52e810c0642323677 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 19 Aug 2023 21:47:31 +0300 Subject: [PATCH 16/80] Move variable declarations to point of initialization --- TacticalAI/DecideAction.cpp | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index fe1f109df..aaf60b765 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -2448,7 +2448,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) INT8 bActionReturned; INT32 iDummy; INT32 iChance; - INT32 sClosestOpponent = NOWHERE, sClosestFriend = NOWHERE; INT32 sClosestDisturbance = NOWHERE, sCheckGridNo; INT32 sDistVisible; UINT8 ubCanMove,ubOpponentDir; @@ -2499,7 +2498,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } // sevenfm: find closest opponent - sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel); + INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel); DebugAI(AI_MSG_INFO, pSoldier, String("sClosestOpponent %d", sClosestOpponent)); if (!SightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE)) @@ -4282,7 +4281,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) #ifdef AI_TIMING_TESTS uiStartTime = GetJA2Clock(); #endif - sClosestFriend = ClosestReachableFriendInTrouble(pSoldier, &fClimb ); + INT32 sClosestFriend = ClosestReachableFriendInTrouble(pSoldier, &fClimb ); #ifdef AI_TIMING_TESTS uiEndTime = GetJA2Clock(); @@ -4897,9 +4896,9 @@ BOOLEAN SoldierCondFalse(SOLDIERTYPE *pSoldier) { return FALSE; } INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) { INT32 iCoverPercentBetter, iOffense, iDefense, iChance; - INT32 sClosestOpponent = NOWHERE,sBestCover = NOWHERE;//dnl ch58 160813 - INT32 sClosestDisturbance; -INT16 ubMinAPCost; + INT32 sBestCover = NOWHERE;//dnl ch58 160813 + INT32 sClosestDisturbance; + INT16 ubMinAPCost; INT8 bDirection; UINT8 ubBestAttackAction = AI_ACTION_NONE; INT8 bCanAttack,bActionReturned; @@ -4917,15 +4916,15 @@ INT16 ubMinAPCost; BOOLEAN fCivilian = (PTR_CIVILIAN && (pSoldier->ubCivilianGroup == NON_CIV_GROUP || pSoldier->aiData.bNeutral || (pSoldier->ubBodyType >= FATCIV && pSoldier->ubBodyType <= CRIPPLECIV) ) ); INT16 ubBurstAPs; UINT8 ubOpponentDir; - INT32 sCheckGridNo; + INT32 sCheckGridNo; BOOLEAN fAllowCoverCheck = FALSE; DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"DecideActionBlack"); - INT32 sOpponentGridNo; - INT8 bOpponentLevel; - sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel); + INT32 sOpponentGridNo; + INT8 bOpponentLevel; + INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel); DebugAI(AI_MSG_INFO, pSoldier, String("sClosestOpponent %d", sClosestOpponent)); // sevenfm: disable stealth mode From 318e4a40cf2c203ff8cacd7abb9dff2db2a48c8a Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 19 Aug 2023 23:23:59 +0300 Subject: [PATCH 17/80] Improve AI logging --- TacticalAI/DecideAction.cpp | 94 ++++++++++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 11 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index aaf60b765..c0c2e9193 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -2529,6 +2529,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) !pSoldier->bBreathCollapsed && pSoldier->IsCowering()) { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop cowering")); return AI_ACTION_STOP_COWERING; } @@ -2540,6 +2541,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) !pSoldier->bBreathCollapsed && pSoldier->IsGivingAid()) { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop giving aid")); return AI_ACTION_STOP_MEDIC; } @@ -2588,7 +2590,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //////////////////////////////////////////////////////////////////////////// // WHEN IN GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND //////////////////////////////////////////////////////////////////////////// - + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide action if stuck in water or gas]")); // when in deep water, move to closest opponent if (ubCanMove && bInDeepWater && !pSoldier->aiData.bNeutral && pSoldier->aiData.bOrders == SEEKENEMY) { @@ -2597,6 +2599,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Move out of water towards closest opponent")); return(AI_ACTION_LEAVE_WATER_GAS); } } @@ -2612,6 +2615,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) AIPopMessage(tempstr); #endif + DebugAI(AI_MSG_INFO, pSoldier, String("Leave for nearest (ungassed) land")); return(AI_ACTION_LEAVE_WATER_GAS); } } @@ -2623,6 +2627,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //if (fCivilian && !(pSoldier->ubBodyType == COW || pSoldier->ubBodyType == CRIPPLECIV || pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE) && gTacticalStatus.bBoxingState == NOT_BOXING) if (fCivilian && !(pSoldier->ubBodyType == COW || pSoldier->ubBodyType == CRIPPLECIV || pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE)) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Civilian decisions]")); if (FindAIUsableObjClass(pSoldier, IC_WEAPON) == NO_SLOT) { // cower in fear!! @@ -2634,6 +2639,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( pSoldier->aiData.bLastAction == AI_ACTION_COWER ) { // do nothing + DebugAI(AI_MSG_INFO, pSoldier, String("Already cowering, do nothing")); pSoldier->aiData.usActionData = NOWHERE; return( AI_ACTION_NONE ); } @@ -2643,12 +2649,14 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->aiData.usNextActionData = FindSpotMaxDistFromOpponents( pSoldier ); if (!TileIsOutOfBounds(pSoldier->aiData.usNextActionData)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop cowering. Prepare for running away")); pSoldier->aiData.bNextAction = AI_ACTION_RUN_AWAY; pSoldier->aiData.usActionData = ANIM_STAND; return( AI_ACTION_STOP_COWERING ); } else { + DebugAI(AI_MSG_INFO, pSoldier, String("Do nothing")); return( AI_ACTION_NONE ); } } @@ -2673,6 +2681,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( gfTurnBasedAI || gTacticalStatus.fEnemyInSector ) { // battle - cower!!! + DebugAI(AI_MSG_INFO, pSoldier, String("Start cowering")); pSoldier->aiData.usActionData = ANIM_CROUCH; return( AI_ACTION_COWER ); } @@ -2856,12 +2865,11 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) !GuySawEnemy(pSoldier, SEEN_LAST_TURN)) { DebugAI(AI_MSG_TOPIC, pSoldier, String("[use smoke to cover friend]")); - DebugAI(AI_MSG_INFO, pSoldier, String("check if we can cover friend with smoke")); CheckTossFriendSmoke(pSoldier, &BestThrow); - if (BestThrow.ubPossible) { + DebugAI(AI_MSG_INFO, pSoldier, String("Throw possible")); DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime)); // start retreating for several turns @@ -2882,6 +2890,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before throw")); pSoldier->aiData.usActionData = BestThrow.ubStance; pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; pSoldier->aiData.usNextActionData = BestThrow.sTarget; @@ -2914,9 +2923,11 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->bAimShotLocation = AIM_SHOT_RANDOM; CheckIfShotPossible(pSoldier, &BestShot); DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: is sniper shot possible? = %d, CTH = %d", BestShot.ubPossible, BestShot.ubChanceToReallyHit)); + DebugAI(AI_MSG_INFO, pSoldier, String("Is sniper shot possible? = %d, CTH = %d", BestShot.ubPossible, BestShot.ubChanceToReallyHit)); if (BestShot.ubPossible && BestShot.ubChanceToReallyHit > 50) { + DebugAI(AI_MSG_INFO, pSoldier, String("Sniper shot possible!")); // then do it! The functions have already made sure that we have a // pair of worthy opponents, etc., so we're not just wasting our time @@ -2936,6 +2947,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } else // snipe not possible { + DebugAI(AI_MSG_INFO, pSoldier, String("Sniper shot NOT possible!")); // if this dude has a long-range weapon on him (longer than normal // sight range), and there's at least one other team-mate around, and // spotters haven't already been called for, then DO SO! @@ -2960,6 +2972,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->bActionPoints = 0; DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: calling for sniper spotters"); + DebugAI(AI_MSG_INFO, pSoldier, String("Call for spotters")); pSoldier->aiData.usActionData = NOWHERE; return(AI_ACTION_NONE); @@ -2990,6 +3003,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) OBJECTTYPE * pAmmo = &(pSoldier->inv[bAmmoSlot]); if ((*pAmmo)[0]->data.ubShotsLeft > pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft && GetAPsToReloadGunWithAmmo(pSoldier, &(pSoldier->inv[BestShot.bWeaponIn]), pAmmo) <= (INT16)pSoldier->bActionPoints) { + DebugAI(AI_MSG_INFO, pSoldier, String("Reload weapon")); pSoldier->aiData.usActionData = BestShot.bWeaponIn; return AI_ACTION_RELOAD_GUN; } @@ -3148,6 +3162,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!LOS_Raised(pSoldier, MercPtrs[BestShot.ubOpponent], CALC_FROM_ALL_DIRS)) ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, New113Message[MSG113_SUPPRESSIONFIRE]); + DebugAI(AI_MSG_INFO, pSoldier, String("Suppression fire!")); return(AI_ACTION_FIRE_GUN); } } @@ -3167,18 +3182,23 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (HAS_SKILL_TRAIT(pSoldier, RADIO_OPERATOR_NT) > 0 && pSoldier->CanUseSkill(SKILLS_RADIO_ARTILLERY, TRUE)) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Radio operator]")); + UINT32 tmp; INT32 skilltargetgridno = 0; // call reinforcements if we haven't yet done so if (!gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition && MoreFriendsThanEnemiesinNearbysectors(pSoldier->bTeam, pSoldier->sSectorX, pSoldier->sSectorY, pSoldier->bSectorZ)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Attempt to call reinforcements")); // if frequencies are jammed... if (SectorJammed()) { + DebugAI(AI_MSG_INFO, pSoldier, String("Someone's jamming radio!")); // if we are jamming, turn it off, otherwise, bad luck... if (pSoldier->IsJamming()) { + DebugAI(AI_MSG_INFO, pSoldier, String("Turn off radio jamming...")); pSoldier->usAISkillUse = SKILLS_RADIO_TURNOFF; pSoldier->aiData.usActionData = skilltargetgridno; return(AI_ACTION_USE_SKILL); @@ -3188,12 +3208,14 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) else if (!(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT)) { // raise alarm! + DebugAI(AI_MSG_INFO, pSoldier, String("Call for reinforcements!")); return(AI_ACTION_RED_ALERT); } } // if we can't call in artillery, jam frequencies, so that the palyer can't use radio skills else if (!pSoldier->IsJamming() && !pSoldier->CanAnyArtilleryStrikeBeOrdered(&tmp)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Start jamming radio frequencies")); pSoldier->usAISkillUse = SKILLS_RADIO_JAM; pSoldier->aiData.usActionData = skilltargetgridno; return(AI_ACTION_USE_SKILL); @@ -3313,16 +3335,20 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if we are a doctor with medical gear, we might be able to help a wounded ally if ( pSoldier->CanMedicAI() ) { - UINT8 ubPerson = GetClosestWoundedSoldierID( pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius, pSoldier->bTeam); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Provide medical aid]")); + UINT8 ubPerson = GetClosestWoundedSoldierID( pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius, pSoldier->bTeam); // are we ourselves the patient? if ( ubPerson == pSoldier->ubID ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Patch ourselves up!")); + // if not already crouched, crouch down first if ( gAnimControl[ pSoldier->usAnimState ].ubHeight != ANIM_CROUCH && IsValidStance( pSoldier, ANIM_CROUCH ) && GetAPsToChangeStance( pSoldier, ANIM_CROUCH ) <= pSoldier->bActionPoints ) { pSoldier->aiData.usActionData = ANIM_CROUCH; + DebugAI(AI_MSG_INFO, pSoldier, String("Crouch down")); return(AI_ACTION_CHANGE_STANCE); } @@ -3330,8 +3356,12 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } else if ( ubPerson != NOBODY ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Someone else is injured")); + if ( PythSpacesAway(pSoldier->sGridNo, MercPtrs[ubPerson]->sGridNo) < 2 ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Wounded soldier is nearby")); + // see if we are facing this person UINT8 ubDesiredMercDir = atan8(CenterX(pSoldier->sGridNo),CenterY(pSoldier->sGridNo),CenterX(MercPtrs[ubPerson]->sGridNo),CenterY(MercPtrs[ubPerson]->sGridNo)); @@ -3340,6 +3370,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { pSoldier->aiData.usActionData = ubDesiredMercDir; + DebugAI(AI_MSG_INFO, pSoldier, String("Change facing")); return( AI_ACTION_CHANGE_FACING ); } @@ -3348,17 +3379,21 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { pSoldier->aiData.usActionData = ANIM_CROUCH; + DebugAI(AI_MSG_INFO, pSoldier, String("Crouch down")); return(AI_ACTION_CHANGE_STANCE); } + DebugAI(AI_MSG_INFO, pSoldier, String("Administer aid")); return(AI_ACTION_DOCTOR); } else { + DebugAI(AI_MSG_INFO, pSoldier, String("Wounded soldier is far")); pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, MercPtrs[ubPerson]->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Try to move towards the wounded person")); return(AI_ACTION_SEEK_FRIEND); } } @@ -3367,20 +3402,25 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if we are not a medic, but are wounded, seek a medic else if ( pSoldier->iHealableInjury >= gGameExternalOptions.sEnemyMedicsWoundMinAmount ) { - UINT8 ubPerson = GetClosestMedicSoldierID( pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius / 2, pSoldier->bTeam); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Seek medical aid]")); + UINT8 ubPerson = GetClosestMedicSoldierID( pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius / 2, pSoldier->bTeam); if ( ubPerson != NOBODY ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Found a medic!")); + if ( PythSpacesAway(pSoldier->sGridNo, MercPtrs[ubPerson]->sGridNo) > 1 ) { pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, MercPtrs[ubPerson]->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek aid")); return(AI_ACTION_SEEK_FRIEND); } } } + else { DebugAI(AI_MSG_INFO, pSoldier, String("No medics around! :(")); } } @@ -3390,20 +3430,25 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // VIPs run away (but not the GENERAL) if ( pSoldier->usSoldierFlagMask & SOLDIER_VIP && pSoldier->ubProfile != GENERAL ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[VIP Retreat]")); + // this is in red AI state - a firefight is going on, we try to escape pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents( pSoldier ); // if we don't know where our opponents are, we cannot run away from them... if ( TileIsOutOfBounds( pSoldier->aiData.usActionData ) ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Don't know where enemies are, head for nearest map edge")); // search for the closest map edge pSoldier->aiData.usActionData = FindClosestExitGrid( pSoldier, pSoldier->sGridNo, 200 ); } if ( !TileIsOutOfBounds( pSoldier->aiData.usActionData ) ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Run away!")); return AI_ACTION_RUN_AWAY; } + else { DebugAI(AI_MSG_INFO, pSoldier, String("No valid gridno found! Tried to head for gridno %d", pSoldier->aiData.usActionData)); } } @@ -3413,18 +3458,22 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // are we a bodyguard? if ( pSoldier->usSoldierFlagMask & SOLDIER_BODYGUARD ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Bodyguard]")); // is VIP still alive? UINT16 ubPerson = GetClosestFlaggedSoldierID( pSoldier, 100, pSoldier->bTeam, SOLDIER_VIP, FALSE ); if ( ubPerson != NOBODY ) { + DebugAI(AI_MSG_INFO, pSoldier, String("VIP found")); // we want to stay close to him, but still be able to function properly... stay withing a 7-tile radius if ( SpacesAway( pSoldier->sGridNo, MercPtrs[ubPerson]->sGridNo ) > 7 ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Attempt to get close ")); pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards( pSoldier, MercPtrs[ubPerson]->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0 ); if ( !TileIsOutOfBounds( pSoldier->aiData.usActionData ) ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek VIP")); return(AI_ACTION_SEEK_FRIEND); } } @@ -3470,6 +3519,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if our breath is running a bit low, and we're not in water or under fire if ((pSoldier->bBreath < 25) && !bInWater && !pSoldier->aiData.bUnderFire) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Running out of breath, try to rest]")); // if not already crouched, try to crouch down first if (!fCivilian && !PTR_CROUCHED && IsValidStance( pSoldier, ANIM_CROUCH ) && gAnimControl[ pSoldier->usAnimState ].ubHeight != ANIM_PRONE) { @@ -3482,6 +3532,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { pSoldier->aiData.usActionData = ANIM_CROUCH; + DebugAI(AI_MSG_INFO, pSoldier, String("Crouch")); return(AI_ACTION_CHANGE_STANCE); } } @@ -3497,8 +3548,11 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( WeaponReady(pSoldier) && GetBPCostPer10APsForGunHolding( pSoldier ) > 0 ) { // unready - return(AI_ACTION_LOWER_GUN); + DebugAI(AI_MSG_INFO, pSoldier, String("Lower weapon")); + return(AI_ACTION_LOWER_GUN); } + + DebugAI(AI_MSG_INFO, pSoldier, String("Rest")); return(AI_ACTION_NONE); } @@ -3512,6 +3566,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if a guy is feeling REALLY discouraged, he may continue to run like hell if ((pSoldier->aiData.bAIMorale == MORALE_HOPELESS) && ubCanMove) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Low morale, attempting to run away]")); DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: run away"); //////////////////////////////////////////////////////////////////////// // RUN AWAY TO SPOT FARTHEST FROM KNOWN THREATS (ONLY IF MORALE HOPELESS) @@ -3526,7 +3581,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) sprintf(tempstr,"%s RUNNING AWAY to grid %d",pSoldier->name,pSoldier->aiData.usActionData); AIPopMessage(tempstr); #endif - + DebugAI(AI_MSG_INFO, pSoldier, String("Running away to grid %d", pSoldier->aiData.usActionData)); return(AI_ACTION_RUN_AWAY); } } @@ -3541,6 +3596,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( !(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT) && !fCivilian && (pSoldier->bActionPoints >= APBPConstants[AP_RADIO]) && (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1) ) { DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: checking to radio red alert"); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Radio red alert]")); // if there hasn't been an initial RED ALERT yet in this sector if ( !(gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition) || NeedToRadioAboutPanicTrigger() ) @@ -3607,6 +3663,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) #endif DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: decided to radio red alert"); + DebugAI(AI_MSG_INFO, pSoldier, String("Decided to radio red alert")); return(AI_ACTION_RED_ALERT); } } @@ -3651,6 +3708,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before throw")); pSoldier->aiData.usActionData = BestThrow.ubStance; pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; pSoldier->aiData.usNextActionData = BestThrow.sTarget; @@ -3665,6 +3723,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->aiData.bAimTime = BestThrow.ubAimTime; } + DebugAI(AI_MSG_INFO, pSoldier, String("Throw smoke!")); return(AI_ACTION_TOSS_PROJECTILE); } } @@ -3692,6 +3751,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { // move as if leaving water or gas + DebugAI(AI_MSG_INFO, pSoldier, String("Move out of light")); return( AI_ACTION_LEAVE_WATER_GAS ); } } @@ -4510,6 +4570,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Take cover")); return(AI_ACTION_TAKE_COVER); } } @@ -5964,7 +6025,6 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) !AnyCoverAtSpot(pSoldier, pSoldier->sGridNo))) { DebugAI(AI_MSG_TOPIC, pSoldier, String("[Black cover advance]")); - DebugAI(AI_MSG_INFO, pSoldier, String("find cover advance spot")); BOOLEAN fClimbDummy; INT32 sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimbDummy); @@ -5984,7 +6044,8 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) // check that we can reach desired location pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sAdvanceSpot, 0, AI_ACTION_GET_CLOSER, 0); - if (pSoldier->aiData.usActionData == sAdvanceSpot) + //if (pSoldier->aiData.usActionData == sAdvanceSpot) + if (pSoldier->aiData.usActionData != NOWHERE) { DebugAI(AI_MSG_INFO, pSoldier, String("cover advance spot ok")); pSoldier->aiData.usActionData = sAdvanceSpot; @@ -6389,6 +6450,8 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) (!gGameExternalOptions.fAISafeSuppression || CheckSuppressionDirection(pSoldier, BestShot.sTarget, BestShot.bTargetLevel))) { pSoldier->aiData.bAimTime--; + if (pSoldier->aiData.bAimTime < 0) { pSoldier->aiData.bAimTime = 0; } + sActualAimAP = CalcAPCostForAiming(pSoldier, BestAttack.sTarget, (INT8)pSoldier->aiData.bAimTime); DebugAI(AI_MSG_INFO, pSoldier, String("reduce aim to %d, recalc autofire, aim AP %d", pSoldier->aiData.bAimTime, sActualAimAP)); goto L_NEWAIM; @@ -10418,15 +10481,18 @@ INT8 DecideActionWearGasmask(SOLDIERTYPE *pSoldier) ActionType DecideActionStuckInWaterOrGas(SOLDIERTYPE *pSoldier, BOOLEAN ubCanMove, BOOLEAN bInWater, BOOLEAN bInDeepWater, BOOLEAN bInGas) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide action if stuck in water or gas]")); + // when in deep water, move to closest opponent if (ubCanMove && (bInDeepWater || bInWater) && !pSoldier->aiData.bNeutral && pSoldier->aiData.bOrders == SEEKENEMY) { // find closest reachable opponent, excluding opponents in deep water - BOOLEAN fClimb; - pSoldier->aiData.usActionData = ClosestReachableDisturbance(pSoldier, &fClimb); + BOOLEAN fClimbDummy; + pSoldier->aiData.usActionData = ClosestReachableDisturbance(pSoldier, &fClimbDummy); if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Move out of water towards closest opponent")); return(AI_ACTION_LEAVE_WATER_GAS); } } @@ -10443,6 +10509,7 @@ ActionType DecideActionStuckInWaterOrGas(SOLDIERTYPE *pSoldier, BOOLEAN ubCanMov AIPopMessage(tempstr); #endif + DebugAI(AI_MSG_INFO, pSoldier, String("Leave for nearest (ungassed) land")); return(AI_ACTION_LEAVE_WATER_GAS); } @@ -10458,6 +10525,7 @@ ActionType DecideActionStuckInWaterOrGas(SOLDIERTYPE *pSoldier, BOOLEAN ubCanMov AIPopMessage(tempstr); #endif + DebugAI(AI_MSG_INFO, pSoldier, String("NO LAND NEAR, RUNNING AWAY to grid %d", pSoldier->aiData.usActionData)); return(AI_ACTION_RUN_AWAY); } @@ -10465,11 +10533,15 @@ ActionType DecideActionStuckInWaterOrGas(SOLDIERTYPE *pSoldier, BOOLEAN ubCanMov // GAS FILLED ROOM (OR IN WATER MORE THAN 25 TILES FROM NEAREST LAND...) if ((bInGas || bInWater || bInDeepWater) && gGameOptions.ubDifficultyLevel == DIF_LEVEL_INSANE) { + DebugAI(AI_MSG_INFO, pSoldier, String("Cornered! Go berserk!")); pSoldier->bBreath = pSoldier->bBreathMax; pSoldier->aiData.bAIMorale = MORALE_FEARLESS; // Can't move, can't get away, go nuts instead... } else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Cornered! Give up on life..")); pSoldier->aiData.bAIMorale = MORALE_HOPELESS; + } } From a45f3ae3191f6d3b59811c0fbd313dab1b6778b5 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 20 Aug 2023 23:38:56 +0300 Subject: [PATCH 18/80] Add more AI logging --- TacticalAI/DecideAction.cpp | 166 ++++++++++++++++++++++++++++++------ 1 file changed, 140 insertions(+), 26 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index c0c2e9193..808712d64 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -2590,8 +2590,8 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //////////////////////////////////////////////////////////////////////////// // WHEN IN GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND //////////////////////////////////////////////////////////////////////////// - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide action if stuck in water or gas]")); // when in deep water, move to closest opponent + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide action if stuck in water or gas]")); if (ubCanMove && bInDeepWater && !pSoldier->aiData.bNeutral && pSoldier->aiData.bOrders == SEEKENEMY) { // find closest reachable opponent, excluding opponents in deep water @@ -2804,6 +2804,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before throw")); pSoldier->aiData.usActionData = BestThrow.ubStance; pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; pSoldier->aiData.usNextActionData = BestThrow.sTarget; @@ -2818,6 +2819,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->aiData.bAimTime = BestThrow.ubAimTime; } + DebugAI(AI_MSG_INFO, pSoldier, String("Throw grenade / use launcher!")); return(AI_ACTION_TOSS_PROJECTILE); } } @@ -2982,6 +2984,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //SUPPRESSION FIRE //CheckIfShotPossible(pSoldier, &BestShot); //WarmSteel - No longer returns 0 when there IS actually a chance to hit. + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Suppression decisions]")); //RELOADING // WarmSteel - Because of suppression fire, we need enough ammo to even consider suppressing @@ -3017,6 +3020,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) INT8 bAmmoSlot = FindAmmoToReload(pSoldier, BestShot.bWeaponIn, NO_SLOT); if (bAmmoSlot != NO_SLOT) { + DebugAI(AI_MSG_INFO, pSoldier, String("Found spare ammo")); fExtraClip = TRUE; } } @@ -3168,6 +3172,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } else { + DebugAI(AI_MSG_INFO, pSoldier, String("Suppression not possible")); pSoldier->bDoBurst = 0; pSoldier->bDoAutofire = 0; } @@ -3291,16 +3296,21 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // Flugente: if we see one of our buddies captured, it is a clear sign of enemy activity! if ( gGameExternalOptions.fAllowPrisonerSystem && pSoldier->bTeam == ENEMY_TEAM ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Free friendly POWs]")); UINT8 ubPerson = GetClosestFlaggedSoldierID( pSoldier, 20, ENEMY_TEAM, SOLDIER_POW, TRUE ); if ( ubPerson != NOBODY ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Found friendly POW")); + // if we are close, we can release this guy // possible only if not handcuffed (binders can be opened, handcuffs not) if ( !HasItemFlag( (&(MercPtrs[ubPerson]->inv[HANDPOS]))->usItem, HANDCUFFS ) ) { if ( PythSpacesAway(pSoldier->sGridNo, MercPtrs[ubPerson]->sGridNo) < 2 ) { + DebugAI(AI_MSG_INFO, pSoldier, String("I am close enough to free POW")); + // see if we are facing this person UINT8 ubDesiredMercDir = atan8(CenterX(pSoldier->sGridNo),CenterY(pSoldier->sGridNo),CenterX(MercPtrs[ubPerson]->sGridNo),CenterY(MercPtrs[ubPerson]->sGridNo)); @@ -3309,9 +3319,11 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { pSoldier->aiData.usActionData = ubDesiredMercDir; + DebugAI(AI_MSG_INFO, pSoldier, String("Change facing")); return( AI_ACTION_CHANGE_FACING ); } + DebugAI(AI_MSG_INFO, pSoldier, String("Free POW")); return(AI_ACTION_FREE_PRISONER); } else @@ -3320,6 +3332,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Move closer to POW")); return(AI_ACTION_SEEK_FRIEND); } } @@ -3599,12 +3612,18 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) DebugAI(AI_MSG_TOPIC, pSoldier, String("[Radio red alert]")); // if there hasn't been an initial RED ALERT yet in this sector - if ( !(gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition) || NeedToRadioAboutPanicTrigger() ) + if (!(gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition) || NeedToRadioAboutPanicTrigger()) + { + DebugAI(AI_MSG_INFO, pSoldier, String("No previous alert radioed")); // since I'm at STATUS RED, I obviously know we're being invaded! iChance = gbDiff[DIFF_RADIO_RED_ALERT][ SoldierDifficultyLevel( pSoldier ) ]; + } else // subsequent radioing (only to update enemy positions, request help) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Red alert already radioed")); // base chance depends on how much new info we have to radio to the others iChance = 10 * WhatIKnowThatPublicDont(pSoldier,FALSE); // use 10 * for RED alert + } // if I actually know something they don't and I ain't swimming (deep water) if (iChance && !bInDeepWater) @@ -3655,6 +3674,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) #ifdef DEBUGDECISIONS AINumMessage("Chance to radio RED alert = ",iChance); #endif + DebugAI(AI_MSG_INFO, pSoldier, String("Chance to radio alert = %d", iChance)); if ((INT16) PreRandom(100) < iChance) { @@ -3686,8 +3706,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) (pSoldier->TakenLargeHit() || pSoldier->ShockLevelPercent() > 20 + Random(80))) { DebugAI(AI_MSG_TOPIC, pSoldier, String("[Self smoke when under fire]")); - DebugAI(AI_MSG_INFO, pSoldier, String("check if soldier can cover himself with smoke")); - CheckTossSelfSmoke(pSoldier, &BestThrow); if (BestThrow.ubPossible) @@ -3726,6 +3744,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) DebugAI(AI_MSG_INFO, pSoldier, String("Throw smoke!")); return(AI_ACTION_TOSS_PROJECTILE); } + else { DebugAI(AI_MSG_INFO, pSoldier, String("Throw not possible")); } } @@ -3779,6 +3798,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) gAnimControl[ pSoldier->usAnimState ].ubHeight != ANIM_PRONE && !pSoldier->aiData.bUnderFire ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Continue flanking]")); DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: continue flanking"); INT16 currDir = GetDirectionFromGridNo ( sFlankGridNo, pSoldier ); INT16 origDir = pSoldier->origDir; @@ -3791,16 +3811,23 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // stop flanking condition if ( (currDir - origDir) >= MinFlankDirections(pSoldier) ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking, left")); pSoldier->numFlanks = MAX_FLANKS_RED; } else { pSoldier->aiData.usActionData = FindFlankingSpot (pSoldier, sFlankGridNo , AI_ACTION_FLANK_LEFT); - if (!TileIsOutOfBounds(pSoldier->aiData.usActionData) ) //&& (currDir - origDir) < 2 ) + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) //&& (currDir - origDir) < 2 ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Flank left")); return AI_ACTION_FLANK_LEFT ; + } else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking left, tile out of bounds")); pSoldier->numFlanks = MAX_FLANKS_RED; + } } } else @@ -3811,16 +3838,23 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // stop flanking condition if ( (origDir - currDir) >= MinFlankDirections(pSoldier) ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking, right")); pSoldier->numFlanks = MAX_FLANKS_RED; } else { pSoldier->aiData.usActionData = FindFlankingSpot (pSoldier, sFlankGridNo , AI_ACTION_FLANK_RIGHT); - if (!TileIsOutOfBounds(pSoldier->aiData.usActionData) )//&& (origDir - currDir) < 2 ) + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))//&& (origDir - currDir) < 2 ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Flank right")); return AI_ACTION_FLANK_RIGHT ; + } else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking right, tile ouf of bounds")); pSoldier->numFlanks = MAX_FLANKS_RED; + } } } } @@ -3832,10 +3866,12 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( pSoldier->numFlanks == MAX_FLANKS_RED ) { DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: stop flanking"); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Stop flanking]")); // start end flank approach with full APs if( gfTurnBasedAI && pSoldier->bActionPoints < pSoldier->bInitialActionPoints ) { + DebugAI(AI_MSG_INFO, pSoldier, String("AP not full, wait a turn")); return(AI_ACTION_END_TURN); } @@ -3847,6 +3883,8 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) ( PythSpacesAway( pSoldier->sGridNo, sFlankGridNo ) > MIN_FLANK_DIST_RED || !LocationToLocationLineOfSightTest( pSoldier->sGridNo, pSoldier->pathing.bLevel, sFlankGridNo, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS) ) ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Move towards enemy")); + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier,sFlankGridNo,GetAPsCrouch( pSoldier, TRUE),AI_ACTION_SEEK_OPPONENT,0); // sevenfm: avoid going into water, gas or light @@ -3859,31 +3897,39 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( LocationToLocationLineOfSightTest( pSoldier->aiData.usActionData, pSoldier->pathing.bLevel, sFlankGridNo, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS) && !LocationToLocationLineOfSightTest( pSoldier->sGridNo, pSoldier->pathing.bLevel, sFlankGridNo, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS) ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Can be seen in new position, prepare crouch & shot")); + // reserve APs for a possible crouch plus a shot INT32 sCautiousGridNo = InternalGoAsFarAsPossibleTowards(pSoldier, sFlankGridNo, (INT8) (MinAPsToAttack( pSoldier, sFlankGridNo, ADDTURNCOST,0) + GetAPsCrouch( pSoldier, TRUE) + GetAPsToLook(pSoldier)), AI_ACTION_SEEK_OPPONENT, FLAG_CAUTIOUS ); if (!TileIsOutOfBounds(sCautiousGridNo)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy to cautiosgridno %d", sCautiousGridNo)); pSoldier->aiData.usActionData = sCautiousGridNo; pSoldier->aiData.fAIFlags |= AI_CAUTIOUS; pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; return(AI_ACTION_SEEK_OPPONENT); } + + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy to gridno %d", pSoldier->aiData.usActionData)); return(AI_ACTION_SEEK_OPPONENT); } else { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy")); return(AI_ACTION_SEEK_OPPONENT); } } else { + DebugAI(AI_MSG_INFO, pSoldier, String("Can't advance, stop flanking")); // if we cannot advance to spot, stop trying pSoldier->numFlanks++; } } else { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking")); // stop pSoldier->numFlanks++; } @@ -3946,6 +3992,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (ubCanMove && pSoldier->bActionPoints > APBPConstants[MAX_AP_CARRIED]) { DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: checking hide/seek/help/watch points... orders = %d, attitude = %d", pSoldier->aiData.bOrders, pSoldier->aiData.bAttitude)); + DebugAI(AI_MSG_INFO, pSoldier, String("checking hide/seek/help/watch points... orders = %d, attitude = %d", pSoldier->aiData.bOrders, pSoldier->aiData.bAttitude)); // calculate initial points for watch based on highest watch loc bWatchPts = GetHighestWatchedLocPoints(pSoldier->ubID); @@ -4042,6 +4089,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } DebugMsg (TOPIC_JA2,DBG_LEVEL_3,String("decideactionred: hide = %d, seek = %d, watch = %d, help = %d",bHidePts,bSeekPts,bWatchPts,bHelpPts)); + DebugAI(AI_MSG_INFO, pSoldier, String("hide = %d, seek = %d, watch = %d, help = %d", bHidePts, bSeekPts, bWatchPts, bHelpPts)); // while one of the three main RED REACTIONS remains viable while ((bSeekPts > -90) || (bHelpPts > -90) || (bHidePts > -90) ) { @@ -4064,6 +4112,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) (gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_PRONE || !GuySawEnemy( pSoldier )) ) { DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: seek opponent"); + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy")); ////////////////////////////////////////////////////////////////////// // SEEK CLOSEST DISTURBANCE: GO DIRECTLY TOWARDS CLOSEST KNOWN OPPONENT ////////////////////////////////////////////////////////////////////// @@ -4113,6 +4162,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if (IsActionAffordable(pSoldier) && pSoldier->bActionPoints >= ( APBPConstants[AP_CLIMBROOF] + MinAPsToAttack( pSoldier, sClosestDisturbance, ADDTURNCOST,0))) { + DebugAI(AI_MSG_INFO, pSoldier, String("Climb roof at gridno %d", sClosestDisturbance)); return( AI_ACTION_CLIMB_ROOF ); } } @@ -4127,6 +4177,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) INT32 usClimbPoint = sClosestDisturbance; if (!TileIsOutOfBounds(usClimbPoint)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Move towards climb spot %d", usClimbPoint)); pSoldier->aiData.usActionData = usClimbPoint; return( AI_ACTION_MOVE_TO_CLIMB ); } @@ -4140,6 +4191,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) BOOLEAN fOvercrowded = FALSE; if( CountNearbyFriends(pSoldier, pSoldier->sGridNo, TACTICAL_RANGE / 4) > 2 ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Soldier position %d is overcrowded", pSoldier->sGridNo)); fOvercrowded = TRUE; } @@ -4158,6 +4210,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->bActionPoints >= APBPConstants[AP_MINIMUM] && ( CountFriendsInDirection( pSoldier, sClosestDisturbance ) > 1 || NightTime() || fOvercrowded) ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Possibly start flanking]")); INT8 action = AI_ACTION_SEEK_OPPONENT; INT16 dist = PythSpacesAway ( pSoldier->sGridNo, sClosestDisturbance ); if ( dist > MIN_FLANK_DIST_RED && dist < MAX_FLANK_DIST_RED ) @@ -4169,26 +4222,37 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) case 1: case 2: case 3: - if ( pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT ) + if (pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Try to flank left")); action = AI_ACTION_FLANK_LEFT ; + } break; default: - if ( pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT ) + if (pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Try to flank right")); action = AI_ACTION_FLANK_RIGHT ; + } break; } if (action == AI_ACTION_SEEK_OPPONENT) { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy instead")); return action; } } else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Distance not suitable, seek enemy instead")); return AI_ACTION_SEEK_OPPONENT ; + } pSoldier->aiData.usActionData = FindFlankingSpot (pSoldier, sClosestDisturbance, action ); if (TileIsOutOfBounds(pSoldier->aiData.usActionData) || pSoldier->numFlanks >= MAX_FLANKS_RED ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Flanking spot %d out of bounds or numFlanks >= MAX_FLANKS_RED", pSoldier->aiData.usActionData)); pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier,sClosestDisturbance,GetAPsCrouch( pSoldier, TRUE), AI_ACTION_SEEK_OPPONENT,0); //pSoldier->numFlanks = 0; if ( PythSpacesAway( pSoldier->aiData.usActionData, sClosestDisturbance ) < 5 || LocationToLocationLineOfSightTest( pSoldier->aiData.usActionData, pSoldier->pathing.bLevel, sClosestDisturbance, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS ) ) @@ -4198,19 +4262,21 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Reserved AP for crouch & shot, seek enemy")); pSoldier->aiData.fAIFlags |= AI_CAUTIOUS; pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; return(AI_ACTION_SEEK_OPPONENT); } } - else { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy")); return(AI_ACTION_SEEK_OPPONENT); } } else { + DebugAI(AI_MSG_INFO, pSoldier, String("Found flanking spot %d", pSoldier->aiData.usActionData)); if ( action == AI_ACTION_FLANK_LEFT ) pSoldier->flags.lastFlankLeft = TRUE; else @@ -4230,11 +4296,13 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->aiData.bOrders = FARPATROL; } + DebugAI(AI_MSG_INFO, pSoldier, String("Start flanking")); return(action); } } else { + DebugAI(AI_MSG_INFO, pSoldier, String("Not flanking, move up towards enemy")); // let's be a bit cautious about going right up to a location without enough APs to shoot if ( PythSpacesAway( pSoldier->aiData.usActionData, sClosestDisturbance ) < 5 || LocationToLocationLineOfSightTest( pSoldier->aiData.usActionData, pSoldier->pathing.bLevel, sClosestDisturbance, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS ) ) { @@ -4243,6 +4311,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Reserved AP for crouch & shot, seek enemy")); pSoldier->aiData.fAIFlags |= AI_CAUTIOUS; pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; return(AI_ACTION_SEEK_OPPONENT); @@ -4250,6 +4319,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } else { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy")); return(AI_ACTION_SEEK_OPPONENT); } break; @@ -4338,6 +4408,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if HELPING is possible and at least as desirable as seeking or hiding if ((bHelpPts > -90) && (bHelpPts >= bSeekPts) && (bHelpPts >= bHidePts) && (bHelpPts >= bWatchPts )) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Help a friend]")); #ifdef AI_TIMING_TESTS uiStartTime = GetJA2Clock(); #endif @@ -4353,6 +4424,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //if (!TileIsOutOfBounds(sClosestFriend) && PythSpacesAway(pSoldier->sGridNo, sClosestFriend) > pSoldier->GetMaxDistanceVisible(sClosestFriend, 0, CALC_FROM_ALL_DIRS )) if (!TileIsOutOfBounds(sClosestFriend)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Closest friend at gridno %d", sClosestFriend)); ////////////////////////////////////////////////////////////////////// // GO DIRECTLY TOWARDS CLOSEST FRIEND UNDER FIRE OR WHO LAST RADIOED ////////////////////////////////////////////////////////////////////// @@ -4365,6 +4437,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->name,sClosestFriend,pSoldier->aiData.usActionData); AIPopMessage(tempstr); #endif + DebugAI(AI_MSG_INFO, pSoldier, String("Seeking friend, moving to %d", pSoldier->aiData.usActionData)); if ( !ENEMYROBOT(pSoldier) && fClimb )//&& pSoldier->aiData.usActionData == sClosestFriend) { @@ -4383,6 +4456,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if (IsActionAffordable(pSoldier) && pSoldier->bActionPoints >= ( APBPConstants[AP_CLIMBROOF] + MinAPsToAttack( pSoldier, sClosestFriend, ADDTURNCOST,0))) { + DebugAI(AI_MSG_INFO, pSoldier, String("Climb roof")); return( AI_ACTION_CLIMB_ROOF ); } } @@ -4393,6 +4467,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //if (!TileIsOutOfBounds(sClimbPoint)) { //pSoldier->aiData.usActionData = sClimbPoint; + DebugAI(AI_MSG_INFO, pSoldier, String("Move towards climb point")); return( AI_ACTION_MOVE_TO_CLIMB ); } } @@ -4401,6 +4476,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //{ // return( AI_ACTION_CLIMB_ROOF ); //} + DebugAI(AI_MSG_INFO, pSoldier, String("Seek friend")); return(AI_ACTION_SEEK_FRIEND); } } @@ -4419,6 +4495,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if HIDING is possible and at least as desirable as seeking or helping if ((bHidePts > -90) && (bHidePts >= bSeekPts) && (bHidePts >= bHelpPts) && (bHidePts >= bWatchPts )) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Take cover]")); //sClosestOpponent = ClosestKnownOpponent( pSoldier, NULL, NULL ); // if an opponent is known (not necessarily reachable or conscious) if (!SkipCoverCheck && !TileIsOutOfBounds(sClosestOpponent)) @@ -4441,6 +4518,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // let's be a bit cautious about going right up to a location without enough APs to shoot if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Found a cover spot at %d", pSoldier->aiData.usActionData)); sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb); if (!TileIsOutOfBounds(sClosestDisturbance) && ( SpacesAway( pSoldier->aiData.usActionData, sClosestDisturbance ) < 5 || SpacesAway( pSoldier->aiData.usActionData, sClosestDisturbance ) + 5 < SpacesAway( pSoldier->sGridNo, sClosestDisturbance ) ) ) { @@ -4448,11 +4526,13 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // ensure will we have enough APs for a possible crouch plus a shot if ( InternalGoAsFarAsPossibleTowards( pSoldier, pSoldier->aiData.usActionData, (INT8) (MinAPsToAttack( pSoldier, sClosestOpponent, ADDTURNCOST,0) + GetAPsCrouch( pSoldier, TRUE)), AI_ACTION_TAKE_COVER, 0 ) == pSoldier->aiData.usActionData ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Moving to cover, reserve AP for crouch & shot")); return(AI_ACTION_TAKE_COVER); } } else { + DebugAI(AI_MSG_INFO, pSoldier, String("Moving to cover")); return(AI_ACTION_TAKE_COVER); } } @@ -4477,6 +4557,8 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if we're currently under fire (presumably, attacker is hidden) if (pSoldier->aiData.bUnderFire) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Under fire]")); + // only try to run if we've actually been hit recently & noticably so // otherwise, presumably our current cover is pretty good & sufficient // HEADROCK HAM B2.6: New value here helps us change the ratio of running away due to shock. This @@ -4497,6 +4579,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (bShock > 0) { + DebugAI(AI_MSG_INFO, pSoldier, String("Soldier is shocked, attempt to run away")); // look for best place to RUN AWAY to (farthest from the closest threat) pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); @@ -4508,6 +4591,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) #endif DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: run away!"); + DebugAI(AI_MSG_INFO, pSoldier, String("Running away to gridno %d", pSoldier->aiData.usActionData)); return(AI_ACTION_RUN_AWAY); } } @@ -4516,6 +4600,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // UNDER FIRE, DON'T WANNA/CAN'T RUN AWAY, SO CROUCH //////////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_INFO, pSoldier, String("Under fire, try to change stance")); DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: crouch or go prone"); // if not in water and not already crouched if (gAnimControl[pSoldier->usAnimState].ubHeight == ANIM_STAND && IsValidStance(pSoldier, ANIM_CROUCH)) @@ -4527,6 +4612,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) sprintf(tempstr, "%s CROUCHES (STATUS RED)", pSoldier->name); AIPopMessage(tempstr); #endif + DebugAI(AI_MSG_INFO, pSoldier, String("Crouching")); pSoldier->aiData.usActionData = ANIM_CROUCH; return(AI_ACTION_CHANGE_STANCE); @@ -4537,6 +4623,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // maybe go prone if (PreRandom(2) == 0 && IsValidStance(pSoldier, ANIM_PRONE)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Go prone")); pSoldier->aiData.usActionData = ANIM_PRONE; return(AI_ACTION_CHANGE_STANCE); } @@ -4559,11 +4646,13 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) DebugAI(AI_MSG_INFO, pSoldier, String("found run away spot %d", pSoldier->aiData.usActionData)); if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { + DebugAI(AI_MSG_INFO, pSoldier, String("Running away!")); return(AI_ACTION_RUN_AWAY); } //else if (!pSoldier->SkipCoverCheck() && gfTurnBasedAI) // only do in turnbased else if (!SkipCoverCheck && gfTurnBasedAI) // only do in turnbased { + DebugAI(AI_MSG_INFO, pSoldier, String("Can't run away, try to take cover")); // try to take cover pSoldier->aiData.bAIMorale = MORALE_WORRIED; pSoldier->aiData.usActionData = FindBestNearbyCover(pSoldier, MORALE_WORRIED, &iDummy); @@ -4589,6 +4678,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(sClosestOpponent)) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Look around towards enemy]")); // determine direction from this soldier to the closest opponent ubOpponentDir = atan8(CenterX(pSoldier->sGridNo),CenterY(pSoldier->sGridNo),CenterX(sClosestOpponent),CenterY(sClosestOpponent)); @@ -4621,13 +4711,15 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) sprintf(tempstr,"%s - TURNS TOWARDS CLOSEST ENEMY to face direction %d",pSoldier->name,pSoldier->aiData.usActionData); AIPopMessage(tempstr); #endif - if ( pSoldier->aiData.bOrders == SNIPER && + DebugAI(AI_MSG_INFO, pSoldier, String("Turn towards closest enemy, face direction %d", pSoldier->aiData.usActionData)); + if ( pSoldier->aiData.bOrders == SNIPER && !WeaponReady(pSoldier) && PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION && (pSoldier->bBreath > 15 || GetBPCostPer10APsForGunHolding( pSoldier, TRUE ) < 50) ) { if (!gfTurnBasedAI || GetAPsToReadyWeapon( pSoldier, READY_RIFLE_CROUCH ) <= pSoldier->bActionPoints) { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, sniper")); pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; } } @@ -4643,13 +4735,13 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if ( Random(100) < 35 ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, scoped weapon")); pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; } } } } //////////////////////////////////////////////////////////////////////////// - return(AI_ACTION_CHANGE_FACING); } } @@ -4659,16 +4751,19 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) !WeaponReady(pSoldier) && PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) { + DebugAI(AI_MSG_INFO, pSoldier, String("Facing enemy already")); if ((!gfTurnBasedAI || GetAPsToReadyWeapon( pSoldier, pSoldier->usAnimState ) <= pSoldier->bActionPoints) && (pSoldier->bBreath > 15 || GetBPCostPer10APsForGunHolding( pSoldier, TRUE ) < 50)) { if ( pSoldier->aiData.bOrders == SNIPER ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, sniper")); return AI_ACTION_RAISE_GUN; } else if (IsScoped(&pSoldier->inv[HANDPOS])) { if ( Random(100) < 40 ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, scoped weapon")); return AI_ACTION_RAISE_GUN; } } @@ -4676,6 +4771,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if ( Random(100) < 20 ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun")); return AI_ACTION_RAISE_GUN; } } @@ -4687,6 +4783,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( ARMED_VEHICLE( pSoldier ) ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Armed vehicle]")); // try turning in a random direction as we still can't see anyone. if (!gfTurnBasedAI || GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints) { @@ -4697,11 +4794,13 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) ubOpponentDir = atan8( CenterX( pSoldier->sGridNo ), CenterY( pSoldier->sGridNo ), CenterX( sClosestDisturbance ), CenterY( sClosestDisturbance ) ); if ( pSoldier->ubDirection == ubOpponentDir ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Already facing closest disturbance, face a random direction")); ubOpponentDir = (UINT8) PreRandom( NUM_WORLD_DIRECTIONS ); } } else { + DebugAI(AI_MSG_INFO, pSoldier, String("Closest disturbance out of bounds, face a random direction")); ubOpponentDir = (UINT8) PreRandom( NUM_WORLD_DIRECTIONS ); } @@ -4721,6 +4820,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if ( gfTurnBasedAI ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Ending turn to limit facing changes")); pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; } else @@ -4730,12 +4830,14 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } + DebugAI(AI_MSG_INFO, pSoldier, String("Turn towards closest disturbance, direction %d", pSoldier->aiData.usActionData)); return(AI_ACTION_CHANGE_FACING); } } } // that's it for tanks + DebugAI(AI_MSG_INFO, pSoldier, String("Do nothing")); return( AI_ACTION_NONE ); } @@ -4763,6 +4865,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) CountFriendsBlack(pSoldier) == 0 ) { // abort! abort! + DebugAI(AI_MSG_INFO, pSoldier, String("Unsafe location, do nothing")); pSoldier->aiData.bAction = AI_ACTION_NONE; } @@ -4819,6 +4922,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if not in water and not already crouched, try to crouch down first if (!fCivilian && !bInWater && (gAnimControl[ pSoldier->usAnimState ].ubHeight == ANIM_STAND) && IsValidStance( pSoldier, ANIM_CROUCH ) ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Crouch]")); //sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); //if ( ( !TileIsOutOfBounds(sClosestOpponent) && PythSpacesAway( pSoldier->sGridNo, sClosestOpponent ) < (MaxNormalDistanceVisible() * 3) / 2 ) || PreRandom( 4 ) == 0 ) @@ -4832,29 +4936,31 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) AIPopMessage(tempstr); #endif - //////////////////////////////////////////////////////////////////////////// - // SANDRO - allow regular soldiers to raise scoped weapons to see farther away too - if (!gfTurnBasedAI || (GetAPsToReadyWeapon( pSoldier, READY_RIFLE_CROUCH ) + GetAPsToChangeStance( pSoldier, ANIM_CROUCH )) <= pSoldier->bActionPoints) - { - // determine direction from this soldier to the closest opponent - ubOpponentDir = atan8(CenterX(pSoldier->sGridNo),CenterY(pSoldier->sGridNo),CenterX(sClosestOpponent),CenterY(sClosestOpponent)); + //////////////////////////////////////////////////////////////////////////// + // SANDRO - allow regular soldiers to raise scoped weapons to see farther away too + if (!gfTurnBasedAI || (GetAPsToReadyWeapon( pSoldier, READY_RIFLE_CROUCH ) + GetAPsToChangeStance( pSoldier, ANIM_CROUCH )) <= pSoldier->bActionPoints) + { + // determine direction from this soldier to the closest opponent + ubOpponentDir = atan8(CenterX(pSoldier->sGridNo),CenterY(pSoldier->sGridNo),CenterX(sClosestOpponent),CenterY(sClosestOpponent)); - if (!WeaponReady(pSoldier) && - pSoldier->ubDirection == ubOpponentDir && - PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) + if (!WeaponReady(pSoldier) && + pSoldier->ubDirection == ubOpponentDir && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) + { + if (IsScoped(&pSoldier->inv[HANDPOS])) { - if (IsScoped(&pSoldier->inv[HANDPOS])) + if ( Random(100) < 40 ) { - if ( Random(100) < 40 ) - { - pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; - } + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, scoped weapon")); + pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; } } } - //////////////////////////////////////////////////////////////////////////// + } + //////////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance to crouch")); pSoldier->aiData.usActionData = ANIM_CROUCH; return(AI_ACTION_CHANGE_STANCE); } @@ -4867,6 +4973,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( !fCivilian && pSoldier->aiData.bUnderFire && pSoldier->bActionPoints >= (pSoldier->bInitialActionPoints - GetAPsToLook( pSoldier ) ) && IsValidStance( pSoldier, ANIM_PRONE ) ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Under fire, go prone]")); sClosestDisturbance = MostImportantNoiseHeard( pSoldier, NULL, NULL, NULL ); if (!TileIsOutOfBounds(sClosestDisturbance)) @@ -4876,6 +4983,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if ( !gfTurnBasedAI || GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Face direction %d", ubOpponentDir)); pSoldier->aiData.usActionData = ubOpponentDir; return( AI_ACTION_CHANGE_FACING ); } @@ -4883,6 +4991,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) else if ( (!gfTurnBasedAI || GetAPsToChangeStance( pSoldier, ANIM_PRONE ) <= pSoldier->bActionPoints ) && pSoldier->InternalIsValidStance( ubOpponentDir, ANIM_PRONE ) ) { // go prone, end turn + DebugAI(AI_MSG_INFO, pSoldier, String("Go prone & end turn")); pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; pSoldier->aiData.usActionData = ANIM_PRONE; return( AI_ACTION_CHANGE_STANCE ); @@ -4896,6 +5005,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //////////////////////////////////////////////////////////////////////////// if ( pSoldier->aiData.bOrders == SNIPER ) { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Sniper]")); if ( pSoldier->sniper == 0 ) { DebugMsg(TOPIC_JA2,DBG_LEVEL_3,String("DecideActionRed: sniper raising gun...")); @@ -4904,6 +5014,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!WeaponReady(pSoldier) && PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, sniper")); pSoldier->sniper = 1; return AI_ACTION_RAISE_GUN; } @@ -4911,6 +5022,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } else { + DebugAI(AI_MSG_INFO, pSoldier, String("Switch to yellow state")); pSoldier->sniper = 0; return(DecideActionYellow(pSoldier)); } @@ -4929,6 +5041,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if ( Random(100) < 35 ) { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun")); return( AI_ACTION_RAISE_GUN ); } } @@ -4945,6 +5058,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) #ifdef DEBUGDECISIONS AINameMessage(pSoldier,"- DOES NOTHING (RED)",1000); #endif + DebugAI(AI_MSG_INFO, pSoldier, String("Do nothing")); pSoldier->aiData.usActionData = NOWHERE; return(AI_ACTION_NONE); From 6131ebc68981416fae92aeb6c0094bde20132340 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 2 Oct 2023 22:20:43 +0300 Subject: [PATCH 19/80] Comment out temp. fixes to flush out AI deadlocks --- TacticalAI/AIMain.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index b2eca435a..fa6eb4631 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -677,10 +677,10 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named // Poor hack to prevent AI deadlocking in case they change stance before firing and player gets an interrupt. // Without this, if player doesn't move any mercs, the AI soldier won't fire and will wait until the deadlock is broken // By canceling the AI action, the AI can reconsider actions and oddly enough, usually decides to fire but this time successfully. - if (pSoldier->aiData.bAction == AI_ACTION_FIRE_GUN && pSoldier->aiData.bLastAction == AI_ACTION_NONE) - { - CancelAIAction(pSoldier, FALSE); - } + //if (pSoldier->aiData.bAction == AI_ACTION_FIRE_GUN && pSoldier->aiData.bLastAction == AI_ACTION_NONE) + //{ + // CancelAIAction(pSoldier, FALSE); + //} pSoldier->aiData.bNewSituation = WAS_NEW_SITUATION; } } @@ -1817,7 +1817,7 @@ void RefreshAI(SOLDIERTYPE *pSoldier) if ((pSoldier->aiData.bAlertStatus == STATUS_BLACK) || (pSoldier->aiData.bAlertStatus == STATUS_RED)) { // always freshly rethink things at start of his turn - CancelAIAction(pSoldier, FALSE); + //CancelAIAction(pSoldier, FALSE); pSoldier->aiData.bNewSituation = IS_NEW_SITUATION; } else From 88af94eaaf0938abc1c89cd8178a9ff2360fe3fb Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 2 Oct 2023 22:26:30 +0300 Subject: [PATCH 20/80] Use Flugente's AI deadlock break & fix wstring for screenMsg --- TacticalAI/AIMain.cpp | 162 +++++++++++++++++++++++++++++++++++------- 1 file changed, 137 insertions(+), 25 deletions(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index fa6eb4631..5a03b1bfd 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -206,6 +206,82 @@ STR szAction[] = { "AI_ACTION_STOP_MEDIC" }; +STR16 wszAction[] = { + L"AI_ACTION_NONE", + + L"AI_ACTION_RANDOM_PATROL", + L"AI_ACTION_SEEK_FRIEND", + L"AI_ACTION_SEEK_OPPONENT", + L"AI_ACTION_TAKE_COVER", + L"AI_ACTION_GET_CLOSER", + + L"AI_ACTION_POINT_PATROL", + L"AI_ACTION_LEAVE_WATER_GAS", + L"AI_ACTION_SEEK_NOISE", + L"AI_ACTION_ESCORTED_MOVE", + L"AI_ACTION_RUN_AWAY", + + L"AI_ACTION_KNIFE_MOVE", + L"AI_ACTION_APPROACH_MERC", + L"AI_ACTION_TRACK", + L"AI_ACTION_EAT", + L"AI_ACTION_PICKUP_ITEM", + + L"AI_ACTION_SCHEDULE_MOVE", + L"AI_ACTION_WALK", + L"AI_ACTION_RUN", + L"AI_ACTION_WITHDRAW", + L"AI_ACTION_FLANK_LEFT", + L"AI_ACTION_FLANK_RIGHT", + L"AI_ACTION_MOVE_TO_CLIMB", + + L"AI_ACTION_CHANGE_FACING", + + L"AI_ACTION_CHANGE_STANCE", + + L"AI_ACTION_YELLOW_ALERT", + L"AI_ACTION_RED_ALERT", + L"AI_ACTION_CREATURE_CALL", + L"AI_ACTION_PULL_TRIGGER", + + L"AI_ACTION_USE_DETONATOR", + L"AI_ACTION_FIRE_GUN", + L"AI_ACTION_TOSS_PROJECTILE", + L"AI_ACTION_KNIFE_STAB", + L"AI_ACTION_THROW_KNIFE", + + L"AI_ACTION_GIVE_AID", + L"AI_ACTION_WAIT", + L"AI_ACTION_PENDING_ACTION", + L"AI_ACTION_DROP_ITEM", + L"AI_ACTION_COWER", + + L"AI_ACTION_STOP_COWERING", + L"AI_ACTION_OPEN_OR_CLOSE_DOOR", + L"AI_ACTION_UNLOCK_DOOR", + L"AI_ACTION_LOCK_DOOR", + L"AI_ACTION_LOWER_GUN", + + L"AI_ACTION_ABSOLUTELY_NONE", + L"AI_ACTION_CLIMB_ROOF", + L"AI_ACTION_END_TURN", + L"AI_ACTION_END_COWER_AND_MOVE", + L"AI_ACTION_TRAVERSE_DOWN", + L"AI_ACTION_OFFER_SURRENDER", + L"AI_ACTION_RAISE_GUN", + L"AI_ACTION_STEAL_MOVE", + + L"AI_ACTION_RELOAD_GUN", + + L"AI_ACTION_JUMP_WINDOW", + L"AI_ACTION_FREE_PRISONER", + L"AI_ACTION_USE_SKILL", + L"AI_ACTION_DOCTOR", + L"AI_ACTION_DOCTOR_SELF", + L"AI_ACTION_SELFDETONATE", + L"AI_ACTION_STOP_MEDIC" +}; + // sevenfm UINT32 guiAIStartCounter = 0, guiAILastCounter = 0; //UINT8 gubAISelectedSoldier = NOBODY; @@ -690,7 +766,6 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named // might have been in 'was' state; no longer so... pSoldier->aiData.bNewSituation = NOT_NEW_SITUATION; } - #ifdef TESTAI DebugMsg( TOPIC_JA2AI, DBG_LEVEL_3,String( ".... HANDLING AI FOR %d",pSoldier->ubID)); #endif @@ -698,42 +773,79 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named /********* Start of new overall AI system ********/ - if (gfTurnBasedAI) { - time_t tCurrentTime = time(0); - UINT32 uiShortDelay = 10; - UINT32 uiDelay = (UINT32)gGameExternalOptions.gubDeadLockDelay; - UINT32 uiTime = (UINT32)(tCurrentTime - gtTimeSinceMercAIStart); - BOOLEAN fKeyPressed = _KeyDown(ESC); + if (true) + { + // added by Flugente: static pointers, used to break out of an endless circles + static SOLDIERTYPE* pLastDecisionSoldier = NULL; + static INT16 lastdecisioncount = 0; + + // simple solution to prevent an endless clock: remember the last soldier that decided an action. If its the same one, increase the counter. + // if counter is high enough, end this guy's turn + if (pSoldier == pLastDecisionSoldier) + { + // we will only end our turn this way if this function was called over 100 times with same soldier without ending a turn. + // so many actions in a single turn smell of an endless clock. + // If we end a turn normally, the counter will be set back to 0, so this wont be a problem if you have a single soldier left for multiple turns + if (lastdecisioncount >= 600) + { + ScreenMsg(FONT_MCOLOR_LTRED, MSG_INTERFACE, L"Aborting AI deadlock for [%d] %s data %d", pSoldier->ubID, wszAction[pSoldier->aiData.bAction], pSoldier->aiData.usActionData); + DebugAI(AI_MSG_INFO, pSoldier, String("Aborting AI deadlock for [%d] %s data %d", pSoldier->ubID, szAction[pSoldier->aiData.bAction], pSoldier->aiData.usActionData)); + DebugAI(AI_MSG_INFO, pSoldier, String("Last action was %s ", szAction[pSoldier->aiData.bLastAction])); - if ((uiTime > uiDelay || uiTime > uiShortDelay && fKeyPressed) && !gfUIInDeadlock) - //if ( ( GetJA2Clock() - gTacticalStatus.uiTimeSinceMercAIStart ) > ( (UINT32)gGameExternalOptions.gubDeadLockDelay * 1000 ) && !gfUIInDeadlock ) + EndAIDeadlock(); + //EndAIGuysTurn(pSoldier); + lastdecisioncount = 0; + return; + } + else + ++lastdecisioncount; + } + else + { + pLastDecisionSoldier = pSoldier; + lastdecisioncount = 0; + } + } + else { - // ATE: Display message that deadlock occured... - LiveMessage( "Breaking Deadlock" ); + time_t tCurrentTime = time(0); + UINT32 uiShortDelay = 10; + UINT32 uiDelay = (UINT32)gGameExternalOptions.gubDeadLockDelay; + UINT32 uiTime = (UINT32)(tCurrentTime - gtTimeSinceMercAIStart); + BOOLEAN fKeyPressed = _KeyDown(ESC); + + if ((uiTime > uiDelay || uiTime > uiShortDelay && fKeyPressed) && !gfUIInDeadlock) + //if ( ( GetJA2Clock() - gTacticalStatus.uiTimeSinceMercAIStart ) > ( (UINT32)gGameExternalOptions.gubDeadLockDelay * 1000 ) && !gfUIInDeadlock ) + { + // ATE: Display message that deadlock occured... + LiveMessage( "Breaking Deadlock" ); + + ScreenMsg(FONT_MCOLOR_LTRED, MSG_INTERFACE, L"Aborting AI deadlock for [%d] %s data %d", pSoldier->ubID, wszAction[pSoldier->aiData.bAction], pSoldier->aiData.usActionData); + DebugAI(AI_MSG_INFO, pSoldier, String("Aborting AI deadlock for [%d] %s data %d", pSoldier->ubID, szAction[pSoldier->aiData.bAction], pSoldier->aiData.usActionData)); + DebugAI(AI_MSG_INFO, pSoldier, String("Last action was %s ", szAction[pSoldier->aiData.bLastAction])); - ScreenMsg(FONT_MCOLOR_LTRED, MSG_INTERFACE, L"Aborting AI deadlock for [%d] %s %s data %d", pSoldier->ubID, pSoldier->GetName(), utf8_to_wstring(std::string(szAction[pSoldier->aiData.bAction])), pSoldier->aiData.usActionData); - DebugAI(String("Aborting AI deadlock for [%d] %s data %d", pSoldier->ubID, szAction[pSoldier->aiData.bAction], pSoldier->aiData.usActionData)); #ifdef JA2TESTVERSION - // display deadlock message - gfUIInDeadlock = TRUE; - gUIDeadlockedSoldier = pSoldier->ubID; - DebugAI( String("DEADLOCK soldier %d action %s ABC %d", pSoldier->ubID, gzActionStr[pSoldier->aiData.bAction], gTacticalStatus.ubAttackBusyCount ) ); + // display deadlock message + gfUIInDeadlock = TRUE; + gUIDeadlockedSoldier = pSoldier->ubID; + DebugAI( String("DEADLOCK soldier %d action %s ABC %d", pSoldier->ubID, gzActionStr[pSoldier->aiData.bAction], gTacticalStatus.ubAttackBusyCount ) ); #else - // If we are in beta version, also report message! + // If we are in beta version, also report message! #ifdef JA2BETAVERSION - ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_ERROR, L"Aborting AI deadlock for %d. Please sent DEBUG.TXT file and SAVE.", pSoldier->ubID ); + ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_ERROR, L"Aborting AI deadlock for %d. Please sent DEBUG.TXT file and SAVE.", pSoldier->ubID ); #endif - // just abort - EndAIDeadlock(); - if ( !(pSoldier->flags.uiStatusFlags & SOLDIER_UNDERAICONTROL) ) - { - return; - } + // just abort + EndAIDeadlock(); + if ( !(pSoldier->flags.uiStatusFlags & SOLDIER_UNDERAICONTROL) ) + { + return; + } #endif + } } } From 1586e8fd2b7586b71e3c99fb2cb2bf6b5c919f5d Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 2 Oct 2023 22:28:56 +0300 Subject: [PATCH 21/80] Allow possibly taking cover without full APs --- TacticalAI/DecideAction.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 808712d64..0ac24fe53 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -6273,7 +6273,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) //////////////////////////////////////////////////////////////////////////// // POSSIBLY FORGET THE ATTACK AND TAKE COVER //////////////////////////////////////////////////////////////////////////// - if ( (pSoldier->bActionPoints == pSoldier->bInitialActionPoints) && + if ( //(pSoldier->bActionPoints == pSoldier->bInitialActionPoints) && (ubBestAttackAction == AI_ACTION_FIRE_GUN) && (pSoldier->aiData.bShock == 0) && (pSoldier->stats.bLife >= pSoldier->stats.bLifeMax / 2) && From d6c9cb20b245d21d5685cf478877c13dd756c229 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 2 Oct 2023 22:29:47 +0300 Subject: [PATCH 22/80] Report soldier ID in DebugMsg when interrupt ends --- Tactical/TeamTurns.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tactical/TeamTurns.cpp b/Tactical/TeamTurns.cpp index ddf9e4430..824c180a9 100644 --- a/Tactical/TeamTurns.cpp +++ b/Tactical/TeamTurns.cpp @@ -1224,10 +1224,10 @@ void EndInterrupt( BOOLEAN fMarkInterruptOccurred ) else { ubInterruptedSoldier = LATEST_INTERRUPT_GUY; + pSoldier = MercPtrs[ubInterruptedSoldier]; - DebugMsg( TOPIC_JA2INTERRUPT, DBG_LEVEL_3, String("INTERRUPT: interrupt over, %d's team regains control", ubInterruptedSoldier ) ); + DebugMsg( TOPIC_JA2INTERRUPT, DBG_LEVEL_3, String("INTERRUPT: interrupt over, soldier %d's team %d regains control", ubInterruptedSoldier, pSoldier->bTeam ) ); - pSoldier = MercPtrs[ubInterruptedSoldier]; cnt = 0; for ( pTempSoldier = MercPtrs[ cnt ]; cnt < MAX_NUM_SOLDIERS; cnt++,pTempSoldier++) From 22f2c9ff9a02992f79f9c4d634fff2dfe7ea684d Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Fri, 2 Feb 2024 01:00:12 +0200 Subject: [PATCH 23/80] Correct AP check We can't do anything with negative action points either. --- TacticalAI/DecideAction.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 638c2b86c..99d4cf163 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -5114,7 +5114,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) } // if we have absolutely no action points, we can't do a thing under BLACK! - if (!pSoldier->bActionPoints) + if (pSoldier->bActionPoints <= 0) { pSoldier->aiData.usActionData = NOWHERE; return(AI_ACTION_NONE); From ffb95a2418a6d04b78d4fdec91d71c747165de61 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 3 Feb 2024 20:49:43 +0200 Subject: [PATCH 24/80] Remove unused variable from if checks aiData.bRTPCombat is never initialized and is always 0 when valid values should be #define RTP_COMBAT_AGGRESSIVE 1 #define RTP_COMBAT_CONSERVE 2 #define RTP_COMBAT_REFRAIN 3 --- TacticalAI/DecideAction.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 99d4cf163..bd494fefd 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -5295,7 +5295,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) //////////////////////////////////////////////////////////////////////////// // NPCs in water/tear gas without masks are not permitted to shoot/stab/throw - if ((pSoldier->bActionPoints < 2) || bInDeepWater || bInGas || pSoldier->aiData.bRTPCombat == RTP_COMBAT_REFRAIN) + if ((pSoldier->bActionPoints < 2) || bInDeepWater || bInGas) { bCanAttack = FALSE; } @@ -6447,7 +6447,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) if (IsGunBurstCapable( &pSoldier->inv[BestAttack.bWeaponIn], FALSE, pSoldier ) && !(Menptr[BestShot.ubOpponent].stats.bLife < OKLIFE) && // don't burst at downed targets pSoldier->inv[BestAttack.bWeaponIn][0]->data.gun.ubGunShotsLeft > 1 && - (pSoldier->bTeam != gbPlayerNum || pSoldier->aiData.bRTPCombat == RTP_COMBAT_AGGRESSIVE) ) + pSoldier->bTeam != gbPlayerNum ) { DebugAI(AI_MSG_INFO, pSoldier, String("enough APs to burst, random chance of doing so")); @@ -9761,7 +9761,7 @@ INT8 ArmedVehicleDecideActionBlack( SOLDIERTYPE *pSoldier ) //////////////////////////////////////////////////////////////////////////// // NPCs in water/tear gas without masks are not permitted to shoot/stab/throw - if ( (pSoldier->bActionPoints < 2) || bInDeepWater || pSoldier->aiData.bRTPCombat == RTP_COMBAT_REFRAIN ) + if ( (pSoldier->bActionPoints < 2) || bInDeepWater ) { bCanAttack = FALSE; } From caf5828153d5d7fa22360e7270ba2832fc601ced Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 4 Feb 2024 12:38:10 +0200 Subject: [PATCH 25/80] Remove deadlock breaking from TurnBasedHandleNPCAI It's already handled before a call to this function, and TurnBasedHandleNPCAI() is only called when pSoldier's action is AI_ACTION_NONE --- TacticalAI/AIMain.cpp | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 5a03b1bfd..c07e84e82 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -1677,33 +1677,6 @@ INT16 ActionInProgress(SOLDIERTYPE *pSoldier) void TurnBasedHandleNPCAI(SOLDIERTYPE *pSoldier) { - // added by Flugente: static pointers, used to break out of an endless circles (currently only used for zombie AI) - static SOLDIERTYPE* pLastDecisionSoldier = NULL; - static INT16 lastdecisioncount = 0; - - // simple solution to prevent an endless clock: remember the last soldier that decided an action. If its the same one, increase the counter. - // if counter is high enough, end this guy's turn - if ( pSoldier == pLastDecisionSoldier ) - { - // we will only end our turn this way if this function was called over 100 times with same soldier without ending a turn. - // so many actions in a single turn smell of an endless clock. - // If we end a turn normally, the counter will be set back to 0, so this wont be a problem if you have a single soldier left for multiple turns - if ( lastdecisioncount >= 100 ) - { - // zombie is done doing harm... - EndAIGuysTurn( pSoldier); - lastdecisioncount = 0; - return ; - } - else - ++lastdecisioncount; - } - else - { - pLastDecisionSoldier = pSoldier; - lastdecisioncount = 0; - } - // yikes, this shouldn't occur! we should be trying to finish our move! // pSoldier->flags.fNoAPToFinishMove = FALSE; @@ -1886,7 +1859,6 @@ void TurnBasedHandleNPCAI(SOLDIERTYPE *pSoldier) { // turn based... abort this guy's turn EndAIGuysTurn( pSoldier ); - lastdecisioncount = 0; } } else From 7ee8165e0255f86336e70d811c4ac00c13070f53 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 4 Feb 2024 14:58:05 +0200 Subject: [PATCH 26/80] Prevent AI deadlocking if NEW_SITUATION is encountered --- TacticalAI/AIMain.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index c07e84e82..01b5ed482 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -750,13 +750,15 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named // make sure we're not using combat AI pSoldier->aiData.bAlertStatus = STATUS_GREEN; } - // Poor hack to prevent AI deadlocking in case they change stance before firing and player gets an interrupt. - // Without this, if player doesn't move any mercs, the AI soldier won't fire and will wait until the deadlock is broken - // By canceling the AI action, the AI can reconsider actions and oddly enough, usually decides to fire but this time successfully. - //if (pSoldier->aiData.bAction == AI_ACTION_FIRE_GUN && pSoldier->aiData.bLastAction == AI_ACTION_NONE) - //{ - // CancelAIAction(pSoldier, FALSE); - //} + + // Prevent AI deadlocking in case enemy is performing an action and player gets an interrupt. + // Without this, if player doesn't move any mercs, the AI soldier will wait until the deadlock is broken. + // By canceling the AI action, the AI can then reconsider actions. + if (pSoldier->aiData.bAction == AI_ACTION_FIRE_GUN || pSoldier->aiData.bAction == AI_ACTION_KNIFE_MOVE || pSoldier->aiData.bAction == AI_ACTION_STEAL_MOVE) + { + DebugAI(AI_MSG_INFO, pSoldier, String("New Situation")); + CancelAIAction(pSoldier, FALSE); + } pSoldier->aiData.bNewSituation = WAS_NEW_SITUATION; } } From f249406ba0ee03909aff38ff605a05680a65c93c Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 4 Feb 2024 15:29:05 +0200 Subject: [PATCH 27/80] Allow specific alert status AI logging Less clutter in the AI logs if, for example one is only interested in BLACK status decisions --- TacticalAI/AIMain.cpp | 5 +- TacticalAI/DecideAction.cpp | 439 +++++++++++++++--------------- TacticalAI/ZombieDecideAction.cpp | 2 +- TacticalAI/ai.h | 2 +- 4 files changed, 225 insertions(+), 223 deletions(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 01b5ed482..0e78ef590 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -286,14 +286,15 @@ STR16 wszAction[] = { UINT32 guiAIStartCounter = 0, guiAILastCounter = 0; //UINT8 gubAISelectedSoldier = NOBODY; BOOLEAN gfLogsEnabled = TRUE; +bool gLogDecideActionRed = false; -void DebugAI( INT8 bMsgType, SOLDIERTYPE *pSoldier, STR szOutput, INT8 bAction ) +void DebugAI( INT8 bMsgType, SOLDIERTYPE *pSoldier, STR szOutput, bool doLog, INT8 bAction) { FILE* DebugFile; CHAR8 msg[1024]; CHAR8 buf[1024]; - if (!gfLogsEnabled) + if (!gfLogsEnabled || !doLog || pSoldier == nullptr) return; memset(buf, 0, 1024 * sizeof(char)); diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index bd494fefd..d8205b121 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -40,12 +40,13 @@ // On the bottom here, there are these functions made ////////////////////////////////////////////////////////////////////// +extern bool gLogDecideActionRed; extern BOOLEAN gfHiddenInterrupt; extern BOOLEAN gfUseAlternateQueenPosition; extern UINT16 PickSoldierReadyAnimation( SOLDIERTYPE *pSoldier, BOOLEAN fEndReady, BOOLEAN fHipStance ); extern void IncrementWatchedLoc(UINT8 ubID, INT32 sGridNo, INT8 bLevel); -void LogDecideInfo(SOLDIERTYPE *pSoldier); -void LogKnowledgeInfo(SOLDIERTYPE *pSoldier); +void LogDecideInfo(SOLDIERTYPE *pSoldier, bool doLog = true); +void LogKnowledgeInfo(SOLDIERTYPE *pSoldier, bool doLog); INT8 DecideActionWearGasmask(SOLDIERTYPE* pSoldier); ActionType DecideActionStuckInWaterOrGas(SOLDIERTYPE* pSoldier, BOOLEAN ubCanMove, BOOLEAN bInWater, BOOLEAN bInDeepWater, BOOLEAN bInGas); @@ -2478,8 +2479,8 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) DebugMsg (TOPIC_JA2,DBG_LEVEL_3,String("DecideActionRed: soldier orders = %d",pSoldier->aiData.bOrders)); - DebugAI(AI_MSG_START, pSoldier, String("[Red]")); - LogDecideInfo(pSoldier); + DebugAI(AI_MSG_START, pSoldier, String("[Red]"), gLogDecideActionRed); + LogDecideInfo(pSoldier, gLogDecideActionRed); // sevenfm: disable stealth mode pSoldier->bStealthMode = FALSE; @@ -2497,18 +2498,18 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // sevenfm: find closest opponent INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel); - DebugAI(AI_MSG_INFO, pSoldier, String("sClosestOpponent %d", sClosestOpponent)); + DebugAI(AI_MSG_INFO, pSoldier, String("sClosestOpponent %d", sClosestOpponent), gLogDecideActionRed); if (!SightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE)) { fCanBeSeen = TRUE; - DebugAI(AI_MSG_INFO, pSoldier, String("can be seen")); + DebugAI(AI_MSG_INFO, pSoldier, String("can be seen"), gLogDecideActionRed); } fProneSightCover = ProneSightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE); - DebugAI(AI_MSG_INFO, pSoldier, String("prone sight cover %d", fProneSightCover)); + DebugAI(AI_MSG_INFO, pSoldier, String("prone sight cover %d", fProneSightCover), gLogDecideActionRed); fAnyCover = AnyCoverAtSpot(pSoldier, pSoldier->sGridNo); - DebugAI(AI_MSG_INFO, pSoldier, String("any cover %d", fAnyCover)); + DebugAI(AI_MSG_INFO, pSoldier, String("any cover %d", fAnyCover), gLogDecideActionRed); if (!fProneSightCover || pSoldier->aiData.bUnderFire) { @@ -2527,7 +2528,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) !pSoldier->bBreathCollapsed && pSoldier->IsCowering()) { - DebugAI(AI_MSG_INFO, pSoldier, String("Stop cowering")); + DebugAI(AI_MSG_INFO, pSoldier, String("Stop cowering"), gLogDecideActionRed); return AI_ACTION_STOP_COWERING; } @@ -2539,7 +2540,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) !pSoldier->bBreathCollapsed && pSoldier->IsGivingAid()) { - DebugAI(AI_MSG_INFO, pSoldier, String("Stop giving aid")); + DebugAI(AI_MSG_INFO, pSoldier, String("Stop giving aid"), gLogDecideActionRed); return AI_ACTION_STOP_MEDIC; } @@ -2589,7 +2590,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // WHEN IN GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND //////////////////////////////////////////////////////////////////////////// // when in deep water, move to closest opponent - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide action if stuck in water or gas]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide action if stuck in water or gas]"), gLogDecideActionRed); if (ubCanMove && bInDeepWater && !pSoldier->aiData.bNeutral && pSoldier->aiData.bOrders == SEEKENEMY) { // find closest reachable opponent, excluding opponents in deep water @@ -2597,7 +2598,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Move out of water towards closest opponent")); + DebugAI(AI_MSG_INFO, pSoldier, String("Move out of water towards closest opponent"), gLogDecideActionRed); return(AI_ACTION_LEAVE_WATER_GAS); } } @@ -2613,7 +2614,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) AIPopMessage(tempstr); #endif - DebugAI(AI_MSG_INFO, pSoldier, String("Leave for nearest (ungassed) land")); + DebugAI(AI_MSG_INFO, pSoldier, String("Leave for nearest (ungassed) land"), gLogDecideActionRed); return(AI_ACTION_LEAVE_WATER_GAS); } } @@ -2625,7 +2626,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //if (fCivilian && !(pSoldier->ubBodyType == COW || pSoldier->ubBodyType == CRIPPLECIV || pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE) && gTacticalStatus.bBoxingState == NOT_BOXING) if (fCivilian && !(pSoldier->ubBodyType == COW || pSoldier->ubBodyType == CRIPPLECIV || pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE)) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Civilian decisions]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Civilian decisions]"), gLogDecideActionRed); if (FindAIUsableObjClass(pSoldier, IC_WEAPON) == NO_SLOT) { // cower in fear!! @@ -2637,7 +2638,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( pSoldier->aiData.bLastAction == AI_ACTION_COWER ) { // do nothing - DebugAI(AI_MSG_INFO, pSoldier, String("Already cowering, do nothing")); + DebugAI(AI_MSG_INFO, pSoldier, String("Already cowering, do nothing"), gLogDecideActionRed); pSoldier->aiData.usActionData = NOWHERE; return( AI_ACTION_NONE ); } @@ -2647,14 +2648,14 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->aiData.usNextActionData = FindSpotMaxDistFromOpponents( pSoldier ); if (!TileIsOutOfBounds(pSoldier->aiData.usNextActionData)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Stop cowering. Prepare for running away")); + DebugAI(AI_MSG_INFO, pSoldier, String("Stop cowering. Prepare for running away"), gLogDecideActionRed); pSoldier->aiData.bNextAction = AI_ACTION_RUN_AWAY; pSoldier->aiData.usActionData = ANIM_STAND; return( AI_ACTION_STOP_COWERING ); } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Do nothing")); + DebugAI(AI_MSG_INFO, pSoldier, String("Do nothing"), gLogDecideActionRed); return( AI_ACTION_NONE ); } } @@ -2679,7 +2680,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( gfTurnBasedAI || gTacticalStatus.fEnemyInSector ) { // battle - cower!!! - DebugAI(AI_MSG_INFO, pSoldier, String("Start cowering")); + DebugAI(AI_MSG_INFO, pSoldier, String("Start cowering"), gLogDecideActionRed); pSoldier->aiData.usActionData = ANIM_CROUCH; return( AI_ACTION_COWER ); } @@ -2710,7 +2711,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) (CanNPCAttack(pSoldier) == TRUE)) { BestThrow.ubPossible = FALSE; // by default, assume Throwing isn't possible - DebugAI(AI_MSG_TOPIC, pSoldier, String("[CheckIfTossPossible]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[CheckIfTossPossible]"), gLogDecideActionRed); CheckIfTossPossible(pSoldier,&BestThrow); @@ -2719,7 +2720,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //////////////////////////////////////////////////////////////////////// if (BestThrow.ubPossible) { - DebugAI(AI_MSG_INFO, pSoldier, String("throw possible")); + DebugAI(AI_MSG_INFO, pSoldier, String("throw possible"), gLogDecideActionRed); // sevenfm: allow using mortars, grenade launchers, flares and grenades in RED state if (Item[pSoldier->inv[BestThrow.bWeaponIn].usItem].mortar || //Item[pSoldier->inv[ BestThrow.bWeaponIn ].usItem].cannon || @@ -2731,7 +2732,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if firing mortar make sure we have room if (Item[pSoldier->inv[BestThrow.bWeaponIn].usItem].mortar) { - DebugAI(AI_MSG_INFO, pSoldier, String("using mortar, check room to deploy")); + DebugAI(AI_MSG_INFO, pSoldier, String("using mortar, check room to deploy"), gLogDecideActionRed); ubOpponentDir = AIDirection(pSoldier->sGridNo, BestThrow.sTarget); // Get new gridno! @@ -2739,7 +2740,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!OKFallDirection(pSoldier, sCheckGridNo, pSoldier->pathing.bLevel, ubOpponentDir, pSoldier->usAnimState)) { - DebugAI(AI_MSG_INFO, pSoldier, String("no room to deploy mortar, check if we can move behind")); + DebugAI(AI_MSG_INFO, pSoldier, String("no room to deploy mortar, check if we can move behind"), gLogDecideActionRed); // can't fire! BestThrow.ubPossible = FALSE; @@ -2752,15 +2753,15 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) INT32 iPathCost = EstimatePlotPath(pSoldier, sCheckGridNo, FALSE, FALSE, FALSE, DetermineMovementMode(pSoldier, AI_ACTION_GET_CLOSER), pSoldier->bStealthMode, FALSE, 0); if (iPathCost != 0 && iPathCost + BestThrow.ubAPCost + GetAPsToLook(pSoldier) + GetAPsCrouch(pSoldier, FALSE) <= pSoldier->bActionPoints) { - DebugAI(AI_MSG_INFO, pSoldier, String("moving backwards to have more room to deploy mortar")); + DebugAI(AI_MSG_INFO, pSoldier, String("moving backwards to have more room to deploy mortar"), gLogDecideActionRed); pSoldier->aiData.usActionData = sCheckGridNo; - DebugAI(AI_MSG_INFO, pSoldier, String("prepare next action throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime)); + DebugAI(AI_MSG_INFO, pSoldier, String("prepare next action throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime), gLogDecideActionRed); // if necessary, swap the usItem if (BestThrow.bWeaponIn != HANDPOS) { - DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket")); + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket"), gLogDecideActionRed); RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); } @@ -2782,19 +2783,19 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if still possible if (BestThrow.ubPossible) { - DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime)); + DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime), gLogDecideActionRed); // if necessary, swap the usItem if (BestThrow.bWeaponIn != HANDPOS) { - DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket")); + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket"), gLogDecideActionRed); RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); } // sevenfm: correctly set weapon mode for attached GL if (IsGrenadeLauncherAttached(&pSoldier->inv[HANDPOS])) { - DebugAI(AI_MSG_INFO, pSoldier, String("set attached GL mode")); + DebugAI(AI_MSG_INFO, pSoldier, String("set attached GL mode"), gLogDecideActionRed); pSoldier->bWeaponMode = WM_ATTACHED_GL; } @@ -2802,7 +2803,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before throw")); + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before throw"), gLogDecideActionRed); pSoldier->aiData.usActionData = BestThrow.ubStance; pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; pSoldier->aiData.usNextActionData = BestThrow.sTarget; @@ -2817,14 +2818,14 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->aiData.bAimTime = BestThrow.ubAimTime; } - DebugAI(AI_MSG_INFO, pSoldier, String("Throw grenade / use launcher!")); + DebugAI(AI_MSG_INFO, pSoldier, String("Throw grenade / use launcher!"), gLogDecideActionRed); return(AI_ACTION_TOSS_PROJECTILE); } } } else // toss/throw/launch not possible { - DebugAI(AI_MSG_INFO, pSoldier, String("throw not possible")); + DebugAI(AI_MSG_INFO, pSoldier, String("throw not possible"), gLogDecideActionRed); // WDS - Fix problem when there is no "best thrown" weapon (i.e., BestThrow.bWeaponIn == NO_SLOT) // if this dude has a longe-range weapon on him (longer than normal // sight range), and there's at least one other team-mate around, and @@ -2835,7 +2836,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1) && (gTacticalStatus.ubSpottersCalledForBy == NOBODY)) { - DebugAI(AI_MSG_INFO, pSoldier, String("throw not possible, call for spotters!")); + DebugAI(AI_MSG_INFO, pSoldier, String("throw not possible, call for spotters!"), gLogDecideActionRed); // then call for spotters! Uses up the rest of his turn (whatever // that may be), but from now on, BLACK AI NPC may radio sightings! @@ -2864,25 +2865,25 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) Chance(100 - min(100, 10 * CountPublicKnownEnemies(pSoldier, pSoldier->sGridNo, TACTICAL_RANGE))) && !GuySawEnemy(pSoldier, SEEN_LAST_TURN)) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[use smoke to cover friend]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[use smoke to cover friend]"), gLogDecideActionRed); CheckTossFriendSmoke(pSoldier, &BestThrow); if (BestThrow.ubPossible) { - DebugAI(AI_MSG_INFO, pSoldier, String("Throw possible")); - DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime)); + DebugAI(AI_MSG_INFO, pSoldier, String("Throw possible"), gLogDecideActionRed); + DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime), gLogDecideActionRed); // start retreating for several turns if (BestThrow.ubOpponent != NOBODY && !MercPtrs[BestThrow.ubOpponent]->IsFlanking()) { - DebugAI(AI_MSG_INFO, pSoldier, String("start retreat counter for %d", BestThrow.ubOpponent)); + DebugAI(AI_MSG_INFO, pSoldier, String("start retreat counter for %d", BestThrow.ubOpponent), gLogDecideActionRed); MercPtrs[BestThrow.ubOpponent]->RetreatCounterStart(2); } // if necessary, swap the usItem from holster into the hand position if (BestThrow.bWeaponIn != HANDPOS) { - DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket")); + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket"), gLogDecideActionRed); RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); } @@ -2890,7 +2891,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before throw")); + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before throw"), gLogDecideActionRed); pSoldier->aiData.usActionData = BestThrow.ubStance; pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; pSoldier->aiData.usNextActionData = BestThrow.sTarget; @@ -2905,7 +2906,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->aiData.bAimTime = BestThrow.ubAimTime; } - DebugAI(AI_MSG_INFO, pSoldier, String("throw smoke grenade to cover friend %d at spot %d level %d", BestThrow.ubOpponent, BestThrow.sTarget, BestThrow.bTargetLevel)); + DebugAI(AI_MSG_INFO, pSoldier, String("throw smoke grenade to cover friend %d at spot %d level %d", BestThrow.ubOpponent, BestThrow.sTarget, BestThrow.bTargetLevel), gLogDecideActionRed); return(AI_ACTION_TOSS_PROJECTILE); } @@ -2923,11 +2924,11 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->bAimShotLocation = AIM_SHOT_RANDOM; CheckIfShotPossible(pSoldier, &BestShot); DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: is sniper shot possible? = %d, CTH = %d", BestShot.ubPossible, BestShot.ubChanceToReallyHit)); - DebugAI(AI_MSG_INFO, pSoldier, String("Is sniper shot possible? = %d, CTH = %d", BestShot.ubPossible, BestShot.ubChanceToReallyHit)); + DebugAI(AI_MSG_INFO, pSoldier, String("Is sniper shot possible? = %d, CTH = %d", BestShot.ubPossible, BestShot.ubChanceToReallyHit), gLogDecideActionRed); if (BestShot.ubPossible && BestShot.ubChanceToReallyHit > 50) { - DebugAI(AI_MSG_INFO, pSoldier, String("Sniper shot possible!")); + DebugAI(AI_MSG_INFO, pSoldier, String("Sniper shot possible!"), gLogDecideActionRed); // then do it! The functions have already made sure that we have a // pair of worthy opponents, etc., so we're not just wasting our time @@ -2947,7 +2948,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } else // snipe not possible { - DebugAI(AI_MSG_INFO, pSoldier, String("Sniper shot NOT possible!")); + DebugAI(AI_MSG_INFO, pSoldier, String("Sniper shot NOT possible!"), gLogDecideActionRed); // if this dude has a long-range weapon on him (longer than normal // sight range), and there's at least one other team-mate around, and // spotters haven't already been called for, then DO SO! @@ -2972,7 +2973,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->bActionPoints = 0; DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: calling for sniper spotters"); - DebugAI(AI_MSG_INFO, pSoldier, String("Call for spotters")); + DebugAI(AI_MSG_INFO, pSoldier, String("Call for spotters"), gLogDecideActionRed); pSoldier->aiData.usActionData = NOWHERE; return(AI_ACTION_NONE); @@ -2982,7 +2983,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //SUPPRESSION FIRE //CheckIfShotPossible(pSoldier, &BestShot); //WarmSteel - No longer returns 0 when there IS actually a chance to hit. - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Suppression decisions]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Suppression decisions]"), gLogDecideActionRed); //RELOADING // WarmSteel - Because of suppression fire, we need enough ammo to even consider suppressing @@ -3004,7 +3005,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) OBJECTTYPE * pAmmo = &(pSoldier->inv[bAmmoSlot]); if ((*pAmmo)[0]->data.ubShotsLeft > pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft && GetAPsToReloadGunWithAmmo(pSoldier, &(pSoldier->inv[BestShot.bWeaponIn]), pAmmo) <= (INT16)pSoldier->bActionPoints) { - DebugAI(AI_MSG_INFO, pSoldier, String("Reload weapon")); + DebugAI(AI_MSG_INFO, pSoldier, String("Reload weapon"), gLogDecideActionRed); pSoldier->aiData.usActionData = BestShot.bWeaponIn; return AI_ACTION_RELOAD_GUN; } @@ -3018,7 +3019,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) INT8 bAmmoSlot = FindAmmoToReload(pSoldier, BestShot.bWeaponIn, NO_SLOT); if (bAmmoSlot != NO_SLOT) { - DebugAI(AI_MSG_INFO, pSoldier, String("Found spare ammo")); + DebugAI(AI_MSG_INFO, pSoldier, String("Found spare ammo"), gLogDecideActionRed); fExtraClip = TRUE; } } @@ -3065,11 +3066,11 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // then do it! // if necessary, swap the usItem from holster into the hand position - DebugAI(AI_MSG_INFO, pSoldier, String("suppression fire possible! target %d level %d aim %d", BestShot.sTarget, BestShot.bTargetLevel, BestShot.ubAimTime)); + DebugAI(AI_MSG_INFO, pSoldier, String("suppression fire possible! target %d level %d aim %d", BestShot.sTarget, BestShot.bTargetLevel, BestShot.ubAimTime), gLogDecideActionRed); if (BestShot.bWeaponIn != HANDPOS) { - DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket")); + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket"), gLogDecideActionRed); RearrangePocket(pSoldier, HANDPOS, BestShot.bWeaponIn, FOREVER); } @@ -3090,7 +3091,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) !UsingNewCTHSystem() && Chance((100 - BestShot.ubChanceToReallyHit) * (100 - BestShot.ubChanceToReallyHit) / 100)) { - DebugAI(AI_MSG_INFO, pSoldier, String("set ubAimTime = 0 for OCTH suppression")); + DebugAI(AI_MSG_INFO, pSoldier, String("set ubAimTime = 0 for OCTH suppression"), gLogDecideActionRed); BestShot.ubAimTime = 0; } @@ -3135,7 +3136,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // Make sure we decided to fire at least one shot! ubBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &(pSoldier->inv[BestShot.bWeaponIn]), pSoldier->bDoAutofire, pSoldier); - DebugAI(AI_MSG_INFO, pSoldier, String("autofire shots %d APcost %d burst AP %d aimtime %d reserve AP %d", pSoldier->bDoAutofire, BestShot.ubAPCost, ubBurstAPs, sActualAimAP, sReserveAP)); + DebugAI(AI_MSG_INFO, pSoldier, String("autofire shots %d APcost %d burst AP %d aimtime %d reserve AP %d", pSoldier->bDoAutofire, BestShot.ubAPCost, ubBurstAPs, sActualAimAP, sReserveAP), gLogDecideActionRed); // minimum 3 bullets if (pSoldier->bDoAutofire >= 3 && pSoldier->bActionPoints >= BestShot.ubAPCost + sActualAimAP + ubBurstAPs + sReserveAP) @@ -3148,7 +3149,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->aiData.bNextTargetLevel = BestShot.bTargetLevel; pSoldier->aiData.usActionData = BestShot.ubStance; - DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before shooting")); + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before shooting"), gLogDecideActionRed); // show "suppression fire" message only if opponent cannot be seen after turning if (!LOS_Raised(pSoldier, MercPtrs[BestShot.ubOpponent], CALC_FROM_ALL_DIRS)) @@ -3164,13 +3165,13 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!LOS_Raised(pSoldier, MercPtrs[BestShot.ubOpponent], CALC_FROM_ALL_DIRS)) ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, New113Message[MSG113_SUPPRESSIONFIRE]); - DebugAI(AI_MSG_INFO, pSoldier, String("Suppression fire!")); + DebugAI(AI_MSG_INFO, pSoldier, String("Suppression fire!"), gLogDecideActionRed); return(AI_ACTION_FIRE_GUN); } } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Suppression not possible")); + DebugAI(AI_MSG_INFO, pSoldier, String("Suppression not possible"), gLogDecideActionRed); pSoldier->bDoBurst = 0; pSoldier->bDoAutofire = 0; } @@ -3185,7 +3186,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (HAS_SKILL_TRAIT(pSoldier, RADIO_OPERATOR_NT) > 0 && pSoldier->CanUseSkill(SKILLS_RADIO_ARTILLERY, TRUE)) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Radio operator]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Radio operator]"), gLogDecideActionRed); UINT32 tmp; INT32 skilltargetgridno = 0; @@ -3193,15 +3194,15 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // call reinforcements if we haven't yet done so if (!gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition && MoreFriendsThanEnemiesinNearbysectors(pSoldier->bTeam, pSoldier->sSectorX, pSoldier->sSectorY, pSoldier->bSectorZ)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Attempt to call reinforcements")); + DebugAI(AI_MSG_INFO, pSoldier, String("Attempt to call reinforcements"), gLogDecideActionRed); // if frequencies are jammed... if (SectorJammed()) { - DebugAI(AI_MSG_INFO, pSoldier, String("Someone's jamming radio!")); + DebugAI(AI_MSG_INFO, pSoldier, String("Someone's jamming radio!"), gLogDecideActionRed); // if we are jamming, turn it off, otherwise, bad luck... if (pSoldier->IsJamming()) { - DebugAI(AI_MSG_INFO, pSoldier, String("Turn off radio jamming...")); + DebugAI(AI_MSG_INFO, pSoldier, String("Turn off radio jamming..."), gLogDecideActionRed); pSoldier->usAISkillUse = SKILLS_RADIO_TURNOFF; pSoldier->aiData.usActionData = skilltargetgridno; return(AI_ACTION_USE_SKILL); @@ -3211,14 +3212,14 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) else if (!(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT)) { // raise alarm! - DebugAI(AI_MSG_INFO, pSoldier, String("Call for reinforcements!")); + DebugAI(AI_MSG_INFO, pSoldier, String("Call for reinforcements!"), gLogDecideActionRed); return(AI_ACTION_RED_ALERT); } } // if we can't call in artillery, jam frequencies, so that the palyer can't use radio skills else if (!pSoldier->IsJamming() && !pSoldier->CanAnyArtilleryStrikeBeOrdered(&tmp)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Start jamming radio frequencies")); + DebugAI(AI_MSG_INFO, pSoldier, String("Start jamming radio frequencies"), gLogDecideActionRed); pSoldier->usAISkillUse = SKILLS_RADIO_JAM; pSoldier->aiData.usActionData = skilltargetgridno; return(AI_ACTION_USE_SKILL); @@ -3294,12 +3295,12 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // Flugente: if we see one of our buddies captured, it is a clear sign of enemy activity! if ( gGameExternalOptions.fAllowPrisonerSystem && pSoldier->bTeam == ENEMY_TEAM ) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Free friendly POWs]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Free friendly POWs]"), gLogDecideActionRed); UINT8 ubPerson = GetClosestFlaggedSoldierID( pSoldier, 20, ENEMY_TEAM, SOLDIER_POW, TRUE ); if ( ubPerson != NOBODY ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Found friendly POW")); + DebugAI(AI_MSG_INFO, pSoldier, String("Found friendly POW"), gLogDecideActionRed); // if we are close, we can release this guy // possible only if not handcuffed (binders can be opened, handcuffs not) @@ -3307,7 +3308,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if ( PythSpacesAway(pSoldier->sGridNo, MercPtrs[ubPerson]->sGridNo) < 2 ) { - DebugAI(AI_MSG_INFO, pSoldier, String("I am close enough to free POW")); + DebugAI(AI_MSG_INFO, pSoldier, String("I am close enough to free POW"), gLogDecideActionRed); // see if we are facing this person UINT8 ubDesiredMercDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, MercPtrs[ubPerson]->sGridNo); @@ -3317,11 +3318,11 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { pSoldier->aiData.usActionData = ubDesiredMercDir; - DebugAI(AI_MSG_INFO, pSoldier, String("Change facing")); + DebugAI(AI_MSG_INFO, pSoldier, String("Change facing"), gLogDecideActionRed); return( AI_ACTION_CHANGE_FACING ); } - DebugAI(AI_MSG_INFO, pSoldier, String("Free POW")); + DebugAI(AI_MSG_INFO, pSoldier, String("Free POW"), gLogDecideActionRed); return(AI_ACTION_FREE_PRISONER); } else @@ -3330,7 +3331,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Move closer to POW")); + DebugAI(AI_MSG_INFO, pSoldier, String("Move closer to POW"), gLogDecideActionRed); return(AI_ACTION_SEEK_FRIEND); } } @@ -3346,20 +3347,20 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if we are a doctor with medical gear, we might be able to help a wounded ally if ( pSoldier->CanMedicAI() ) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Provide medical aid]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Provide medical aid]"), gLogDecideActionRed); UINT8 ubPerson = GetClosestWoundedSoldierID( pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius, pSoldier->bTeam); // are we ourselves the patient? if ( ubPerson == pSoldier->ubID ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Patch ourselves up!")); + DebugAI(AI_MSG_INFO, pSoldier, String("Patch ourselves up!"), gLogDecideActionRed); // if not already crouched, crouch down first if ( gAnimControl[ pSoldier->usAnimState ].ubHeight != ANIM_CROUCH && IsValidStance( pSoldier, ANIM_CROUCH ) && GetAPsToChangeStance( pSoldier, ANIM_CROUCH ) <= pSoldier->bActionPoints ) { pSoldier->aiData.usActionData = ANIM_CROUCH; - DebugAI(AI_MSG_INFO, pSoldier, String("Crouch down")); + DebugAI(AI_MSG_INFO, pSoldier, String("Crouch down"), gLogDecideActionRed); return(AI_ACTION_CHANGE_STANCE); } @@ -3367,11 +3368,11 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } else if ( ubPerson != NOBODY ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Someone else is injured")); + DebugAI(AI_MSG_INFO, pSoldier, String("Someone else is injured"), gLogDecideActionRed); if ( PythSpacesAway(pSoldier->sGridNo, MercPtrs[ubPerson]->sGridNo) < 2 ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Wounded soldier is nearby")); + DebugAI(AI_MSG_INFO, pSoldier, String("Wounded soldier is nearby"), gLogDecideActionRed); // see if we are facing this person UINT8 ubDesiredMercDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, MercPtrs[ubPerson]->sGridNo); @@ -3381,7 +3382,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { pSoldier->aiData.usActionData = ubDesiredMercDir; - DebugAI(AI_MSG_INFO, pSoldier, String("Change facing")); + DebugAI(AI_MSG_INFO, pSoldier, String("Change facing"), gLogDecideActionRed); return( AI_ACTION_CHANGE_FACING ); } @@ -3390,21 +3391,21 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { pSoldier->aiData.usActionData = ANIM_CROUCH; - DebugAI(AI_MSG_INFO, pSoldier, String("Crouch down")); + DebugAI(AI_MSG_INFO, pSoldier, String("Crouch down"), gLogDecideActionRed); return(AI_ACTION_CHANGE_STANCE); } - DebugAI(AI_MSG_INFO, pSoldier, String("Administer aid")); + DebugAI(AI_MSG_INFO, pSoldier, String("Administer aid"), gLogDecideActionRed); return(AI_ACTION_DOCTOR); } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Wounded soldier is far")); + DebugAI(AI_MSG_INFO, pSoldier, String("Wounded soldier is far"), gLogDecideActionRed); pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, MercPtrs[ubPerson]->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Try to move towards the wounded person")); + DebugAI(AI_MSG_INFO, pSoldier, String("Try to move towards the wounded person"), gLogDecideActionRed); return(AI_ACTION_SEEK_FRIEND); } } @@ -3413,12 +3414,12 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if we are not a medic, but are wounded, seek a medic else if ( pSoldier->iHealableInjury >= gGameExternalOptions.sEnemyMedicsWoundMinAmount ) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Seek medical aid]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Seek medical aid]"), gLogDecideActionRed); UINT8 ubPerson = GetClosestMedicSoldierID( pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius / 2, pSoldier->bTeam); if ( ubPerson != NOBODY ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Found a medic!")); + DebugAI(AI_MSG_INFO, pSoldier, String("Found a medic!"), gLogDecideActionRed); if ( PythSpacesAway(pSoldier->sGridNo, MercPtrs[ubPerson]->sGridNo) > 1 ) { @@ -3426,12 +3427,12 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Seek aid")); + DebugAI(AI_MSG_INFO, pSoldier, String("Seek aid"), gLogDecideActionRed); return(AI_ACTION_SEEK_FRIEND); } } } - else { DebugAI(AI_MSG_INFO, pSoldier, String("No medics around! :(")); } + else { DebugAI(AI_MSG_INFO, pSoldier, String("No medics around! :("), gLogDecideActionRed); } } @@ -3441,7 +3442,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // VIPs run away (but not the GENERAL) if ( pSoldier->usSoldierFlagMask & SOLDIER_VIP && pSoldier->ubProfile != GENERAL ) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[VIP Retreat]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[VIP Retreat]"), gLogDecideActionRed); // this is in red AI state - a firefight is going on, we try to escape pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents( pSoldier ); @@ -3449,17 +3450,17 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if we don't know where our opponents are, we cannot run away from them... if ( TileIsOutOfBounds( pSoldier->aiData.usActionData ) ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Don't know where enemies are, head for nearest map edge")); + DebugAI(AI_MSG_INFO, pSoldier, String("Don't know where enemies are, head for nearest map edge"), gLogDecideActionRed); // search for the closest map edge pSoldier->aiData.usActionData = FindClosestExitGrid( pSoldier, pSoldier->sGridNo, 200 ); } if ( !TileIsOutOfBounds( pSoldier->aiData.usActionData ) ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Run away!")); + DebugAI(AI_MSG_INFO, pSoldier, String("Run away!"), gLogDecideActionRed); return AI_ACTION_RUN_AWAY; } - else { DebugAI(AI_MSG_INFO, pSoldier, String("No valid gridno found! Tried to head for gridno %d", pSoldier->aiData.usActionData)); } + else { DebugAI(AI_MSG_INFO, pSoldier, String("No valid gridno found! Tried to head for gridno %d", pSoldier->aiData.usActionData), gLogDecideActionRed); } } @@ -3469,22 +3470,22 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // are we a bodyguard? if ( pSoldier->usSoldierFlagMask & SOLDIER_BODYGUARD ) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Bodyguard]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Bodyguard]"), gLogDecideActionRed); // is VIP still alive? UINT16 ubPerson = GetClosestFlaggedSoldierID( pSoldier, 100, pSoldier->bTeam, SOLDIER_VIP, FALSE ); if ( ubPerson != NOBODY ) { - DebugAI(AI_MSG_INFO, pSoldier, String("VIP found")); + DebugAI(AI_MSG_INFO, pSoldier, String("VIP found"), gLogDecideActionRed); // we want to stay close to him, but still be able to function properly... stay withing a 7-tile radius if ( SpacesAway( pSoldier->sGridNo, MercPtrs[ubPerson]->sGridNo ) > 7 ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Attempt to get close ")); + DebugAI(AI_MSG_INFO, pSoldier, String("Attempt to get close "), gLogDecideActionRed); pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards( pSoldier, MercPtrs[ubPerson]->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0 ); if ( !TileIsOutOfBounds( pSoldier->aiData.usActionData ) ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Seek VIP")); + DebugAI(AI_MSG_INFO, pSoldier, String("Seek VIP"), gLogDecideActionRed); return(AI_ACTION_SEEK_FRIEND); } } @@ -3506,13 +3507,13 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->RetreatCounterValue() > 0 && (pSoldier->CheckInitialAP() || !fAnyCover || pSoldier->aiData.bUnderFire)) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[retreat]")); - DebugAI(AI_MSG_TOPIC, pSoldier, String("search for retreat spot")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[retreat]"), gLogDecideActionRed); + DebugAI(AI_MSG_TOPIC, pSoldier, String("search for retreat spot"), gLogDecideActionRed); INT32 sRetreatSpot = FindRetreatSpot(pSoldier); if (!TileIsOutOfBounds(sRetreatSpot)) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("found retreat spot %d", sRetreatSpot)); + DebugAI(AI_MSG_TOPIC, pSoldier, String("found retreat spot %d", sRetreatSpot), gLogDecideActionRed); //BeginMultiPurposeLocator(sRetreatSpot, pSoldier->pathing.bLevel, FALSE); @@ -3530,7 +3531,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if our breath is running a bit low, and we're not in water or under fire if ((pSoldier->bBreath < 25) && !bInWater && !pSoldier->aiData.bUnderFire) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Running out of breath, try to rest]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Running out of breath, try to rest]"), gLogDecideActionRed); // if not already crouched, try to crouch down first if (!fCivilian && !PTR_CROUCHED && IsValidStance( pSoldier, ANIM_CROUCH ) && gAnimControl[ pSoldier->usAnimState ].ubHeight != ANIM_PRONE) { @@ -3543,7 +3544,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { pSoldier->aiData.usActionData = ANIM_CROUCH; - DebugAI(AI_MSG_INFO, pSoldier, String("Crouch")); + DebugAI(AI_MSG_INFO, pSoldier, String("Crouch"), gLogDecideActionRed); return(AI_ACTION_CHANGE_STANCE); } } @@ -3559,11 +3560,11 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( WeaponReady(pSoldier) && GetBPCostPer10APsForGunHolding( pSoldier ) > 0 ) { // unready - DebugAI(AI_MSG_INFO, pSoldier, String("Lower weapon")); + DebugAI(AI_MSG_INFO, pSoldier, String("Lower weapon"), gLogDecideActionRed); return(AI_ACTION_LOWER_GUN); } - DebugAI(AI_MSG_INFO, pSoldier, String("Rest")); + DebugAI(AI_MSG_INFO, pSoldier, String("Rest"), gLogDecideActionRed); return(AI_ACTION_NONE); } @@ -3577,7 +3578,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if a guy is feeling REALLY discouraged, he may continue to run like hell if ((pSoldier->aiData.bAIMorale == MORALE_HOPELESS) && ubCanMove) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Low morale, attempting to run away]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Low morale, attempting to run away]"), gLogDecideActionRed); DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: run away"); //////////////////////////////////////////////////////////////////////// // RUN AWAY TO SPOT FARTHEST FROM KNOWN THREATS (ONLY IF MORALE HOPELESS) @@ -3592,7 +3593,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) sprintf(tempstr,"%s RUNNING AWAY to grid %d",pSoldier->name,pSoldier->aiData.usActionData); AIPopMessage(tempstr); #endif - DebugAI(AI_MSG_INFO, pSoldier, String("Running away to grid %d", pSoldier->aiData.usActionData)); + DebugAI(AI_MSG_INFO, pSoldier, String("Running away to grid %d", pSoldier->aiData.usActionData), gLogDecideActionRed); return(AI_ACTION_RUN_AWAY); } } @@ -3607,18 +3608,18 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( !(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT) && !fCivilian && (pSoldier->bActionPoints >= APBPConstants[AP_RADIO]) && (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1) ) { DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: checking to radio red alert"); - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Radio red alert]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Radio red alert]"), gLogDecideActionRed); // if there hasn't been an initial RED ALERT yet in this sector if (!(gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition) || NeedToRadioAboutPanicTrigger()) { - DebugAI(AI_MSG_INFO, pSoldier, String("No previous alert radioed")); + DebugAI(AI_MSG_INFO, pSoldier, String("No previous alert radioed"), gLogDecideActionRed); // since I'm at STATUS RED, I obviously know we're being invaded! iChance = gbDiff[DIFF_RADIO_RED_ALERT][ SoldierDifficultyLevel( pSoldier ) ]; } else // subsequent radioing (only to update enemy positions, request help) { - DebugAI(AI_MSG_INFO, pSoldier, String("Red alert already radioed")); + DebugAI(AI_MSG_INFO, pSoldier, String("Red alert already radioed"), gLogDecideActionRed); // base chance depends on how much new info we have to radio to the others iChance = 10 * WhatIKnowThatPublicDont(pSoldier,FALSE); // use 10 * for RED alert } @@ -3672,7 +3673,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) #ifdef DEBUGDECISIONS AINumMessage("Chance to radio RED alert = ",iChance); #endif - DebugAI(AI_MSG_INFO, pSoldier, String("Chance to radio alert = %d", iChance)); + DebugAI(AI_MSG_INFO, pSoldier, String("Chance to radio alert = %d", iChance), gLogDecideActionRed); if ((INT16) PreRandom(100) < iChance) { @@ -3681,7 +3682,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) #endif DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: decided to radio red alert"); - DebugAI(AI_MSG_INFO, pSoldier, String("Decided to radio red alert")); + DebugAI(AI_MSG_INFO, pSoldier, String("Decided to radio red alert"), gLogDecideActionRed); return(AI_ACTION_RED_ALERT); } } @@ -3703,12 +3704,12 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) (!fProneSightCover && !AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || pSoldier->TakenLargeHit()) && (pSoldier->TakenLargeHit() || pSoldier->ShockLevelPercent() > 20 + Random(80))) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Self smoke when under fire]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Self smoke when under fire]"), gLogDecideActionRed); CheckTossSelfSmoke(pSoldier, &BestThrow); if (BestThrow.ubPossible) { - DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime)); + DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime), gLogDecideActionRed); // start retreating for several turns pSoldier->RetreatCounterStart(2); @@ -3716,7 +3717,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if necessary, swap the usItem from holster into the hand position if (BestThrow.bWeaponIn != HANDPOS) { - DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket")); + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket"), gLogDecideActionRed); RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); } @@ -3724,7 +3725,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before throw")); + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before throw"), gLogDecideActionRed); pSoldier->aiData.usActionData = BestThrow.ubStance; pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; pSoldier->aiData.usNextActionData = BestThrow.sTarget; @@ -3739,10 +3740,10 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->aiData.bAimTime = BestThrow.ubAimTime; } - DebugAI(AI_MSG_INFO, pSoldier, String("Throw smoke!")); + DebugAI(AI_MSG_INFO, pSoldier, String("Throw smoke!"), gLogDecideActionRed); return(AI_ACTION_TOSS_PROJECTILE); } - else { DebugAI(AI_MSG_INFO, pSoldier, String("Throw not possible")); } + else { DebugAI(AI_MSG_INFO, pSoldier, String("Throw not possible"), gLogDecideActionRed); } } @@ -3768,7 +3769,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { // move as if leaving water or gas - DebugAI(AI_MSG_INFO, pSoldier, String("Move out of light")); + DebugAI(AI_MSG_INFO, pSoldier, String("Move out of light"), gLogDecideActionRed); return( AI_ACTION_LEAVE_WATER_GAS ); } } @@ -3796,7 +3797,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) gAnimControl[ pSoldier->usAnimState ].ubHeight != ANIM_PRONE && !pSoldier->aiData.bUnderFire ) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Continue flanking]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Continue flanking]"), gLogDecideActionRed); DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: continue flanking"); INT16 currDir = GetDirectionFromGridNo ( sFlankGridNo, pSoldier ); INT16 origDir = pSoldier->origDir; @@ -3809,7 +3810,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // stop flanking condition if ( (currDir - origDir) >= MinFlankDirections(pSoldier) ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking, left")); + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking, left"), gLogDecideActionRed); pSoldier->numFlanks = MAX_FLANKS_RED; } else @@ -3818,12 +3819,12 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) //&& (currDir - origDir) < 2 ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Flank left")); + DebugAI(AI_MSG_INFO, pSoldier, String("Flank left"), gLogDecideActionRed); return AI_ACTION_FLANK_LEFT ; } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking left, tile out of bounds")); + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking left, tile out of bounds"), gLogDecideActionRed); pSoldier->numFlanks = MAX_FLANKS_RED; } } @@ -3836,7 +3837,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // stop flanking condition if ( (origDir - currDir) >= MinFlankDirections(pSoldier) ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking, right")); + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking, right"), gLogDecideActionRed); pSoldier->numFlanks = MAX_FLANKS_RED; } else @@ -3845,12 +3846,12 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))//&& (origDir - currDir) < 2 ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Flank right")); + DebugAI(AI_MSG_INFO, pSoldier, String("Flank right"), gLogDecideActionRed); return AI_ACTION_FLANK_RIGHT ; } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking right, tile ouf of bounds")); + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking right, tile ouf of bounds"), gLogDecideActionRed); pSoldier->numFlanks = MAX_FLANKS_RED; } } @@ -3864,12 +3865,12 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( pSoldier->numFlanks == MAX_FLANKS_RED ) { DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: stop flanking"); - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Stop flanking]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Stop flanking]"), gLogDecideActionRed); // start end flank approach with full APs if( gfTurnBasedAI && pSoldier->bActionPoints < pSoldier->bInitialActionPoints ) { - DebugAI(AI_MSG_INFO, pSoldier, String("AP not full, wait a turn")); + DebugAI(AI_MSG_INFO, pSoldier, String("AP not full, wait a turn"), gLogDecideActionRed); return(AI_ACTION_END_TURN); } @@ -3881,7 +3882,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) ( PythSpacesAway( pSoldier->sGridNo, sFlankGridNo ) > MIN_FLANK_DIST_RED || !LocationToLocationLineOfSightTest( pSoldier->sGridNo, pSoldier->pathing.bLevel, sFlankGridNo, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS) ) ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Move towards enemy")); + DebugAI(AI_MSG_INFO, pSoldier, String("Move towards enemy"), gLogDecideActionRed); pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier,sFlankGridNo,GetAPsCrouch( pSoldier, TRUE),AI_ACTION_SEEK_OPPONENT,0); @@ -3895,39 +3896,39 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( LocationToLocationLineOfSightTest( pSoldier->aiData.usActionData, pSoldier->pathing.bLevel, sFlankGridNo, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS) && !LocationToLocationLineOfSightTest( pSoldier->sGridNo, pSoldier->pathing.bLevel, sFlankGridNo, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS) ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Can be seen in new position, prepare crouch & shot")); + DebugAI(AI_MSG_INFO, pSoldier, String("Can be seen in new position, prepare crouch & shot"), gLogDecideActionRed); // reserve APs for a possible crouch plus a shot INT32 sCautiousGridNo = InternalGoAsFarAsPossibleTowards(pSoldier, sFlankGridNo, (INT8) (MinAPsToAttack( pSoldier, sFlankGridNo, ADDTURNCOST,0) + GetAPsCrouch( pSoldier, TRUE) + GetAPsToLook(pSoldier)), AI_ACTION_SEEK_OPPONENT, FLAG_CAUTIOUS ); if (!TileIsOutOfBounds(sCautiousGridNo)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy to cautiosgridno %d", sCautiousGridNo)); + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy to cautiosgridno %d", sCautiousGridNo), gLogDecideActionRed); pSoldier->aiData.usActionData = sCautiousGridNo; pSoldier->aiData.fAIFlags |= AI_CAUTIOUS; pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; return(AI_ACTION_SEEK_OPPONENT); } - DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy to gridno %d", pSoldier->aiData.usActionData)); + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy to gridno %d", pSoldier->aiData.usActionData), gLogDecideActionRed); return(AI_ACTION_SEEK_OPPONENT); } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy")); + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy"), gLogDecideActionRed); return(AI_ACTION_SEEK_OPPONENT); } } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Can't advance, stop flanking")); + DebugAI(AI_MSG_INFO, pSoldier, String("Can't advance, stop flanking"), gLogDecideActionRed); // if we cannot advance to spot, stop trying pSoldier->numFlanks++; } } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking")); + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking"), gLogDecideActionRed); // stop pSoldier->numFlanks++; } @@ -3948,7 +3949,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) !SoldierToVirtualSoldierLineOfSightTest(pSoldier, sClosestDisturbance, pSoldier->pathing.bLevel, ANIM_STAND, TRUE, CALC_FROM_ALL_DIRS) && CountFriendsBlack(pSoldier, sClosestDisturbance) == 0) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Set watched location]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Set watched location]"), gLogDecideActionRed); gubNPCAPBudget = 0; gubNPCDistLimit = 0; @@ -3958,8 +3959,8 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) INT16 sLoop; INT32 sLastSeenSpot = NOWHERE; - DebugAI(AI_MSG_INFO, pSoldier, String("found path to %d, path size %d ", sClosestDisturbance, pSoldier->pathing.usPathDataSize)); - DebugAI(AI_MSG_INFO, pSoldier, String("check path for seen spots")); + DebugAI(AI_MSG_INFO, pSoldier, String("found path to %d, path size %d ", sClosestDisturbance, pSoldier->pathing.usPathDataSize), gLogDecideActionRed); + DebugAI(AI_MSG_INFO, pSoldier, String("check path for seen spots"), gLogDecideActionRed); sCheckGridNo = pSoldier->sGridNo; @@ -3976,7 +3977,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if found last seen spot if (!TileIsOutOfBounds(sLastSeenSpot)) { - DebugAI(AI_MSG_INFO, pSoldier, String("last seen spot %d level %d", sLastSeenSpot, pSoldier->pathing.bLevel)); + DebugAI(AI_MSG_INFO, pSoldier, String("last seen spot %d level %d", sLastSeenSpot, pSoldier->pathing.bLevel), gLogDecideActionRed); IncrementWatchedLoc(pSoldier->ubID, sLastSeenSpot, pSoldier->pathing.bLevel); } } @@ -3990,7 +3991,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (ubCanMove && pSoldier->bActionPoints > APBPConstants[MAX_AP_CARRIED]) { DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: checking hide/seek/help/watch points... orders = %d, attitude = %d", pSoldier->aiData.bOrders, pSoldier->aiData.bAttitude)); - DebugAI(AI_MSG_INFO, pSoldier, String("checking hide/seek/help/watch points... orders = %d, attitude = %d", pSoldier->aiData.bOrders, pSoldier->aiData.bAttitude)); + DebugAI(AI_MSG_INFO, pSoldier, String("checking hide/seek/help/watch points... orders = %d, attitude = %d", pSoldier->aiData.bOrders, pSoldier->aiData.bAttitude), gLogDecideActionRed); // calculate initial points for watch based on highest watch loc bWatchPts = GetHighestWatchedLocPoints(pSoldier->ubID); @@ -4087,7 +4088,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } DebugMsg (TOPIC_JA2,DBG_LEVEL_3,String("decideactionred: hide = %d, seek = %d, watch = %d, help = %d",bHidePts,bSeekPts,bWatchPts,bHelpPts)); - DebugAI(AI_MSG_INFO, pSoldier, String("hide = %d, seek = %d, watch = %d, help = %d", bHidePts, bSeekPts, bWatchPts, bHelpPts)); + DebugAI(AI_MSG_INFO, pSoldier, String("hide = %d, seek = %d, watch = %d, help = %d", bHidePts, bSeekPts, bWatchPts, bHelpPts), gLogDecideActionRed); // while one of the three main RED REACTIONS remains viable while ((bSeekPts > -90) || (bHelpPts > -90) || (bHidePts > -90) ) { @@ -4110,7 +4111,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) (gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_PRONE || !GuySawEnemy( pSoldier )) ) { DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: seek opponent"); - DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy")); + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy"), gLogDecideActionRed); ////////////////////////////////////////////////////////////////////// // SEEK CLOSEST DISTURBANCE: GO DIRECTLY TOWARDS CLOSEST KNOWN OPPONENT ////////////////////////////////////////////////////////////////////// @@ -4160,7 +4161,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if (IsActionAffordable(pSoldier) && pSoldier->bActionPoints >= ( APBPConstants[AP_CLIMBROOF] + MinAPsToAttack( pSoldier, sClosestDisturbance, ADDTURNCOST,0))) { - DebugAI(AI_MSG_INFO, pSoldier, String("Climb roof at gridno %d", sClosestDisturbance)); + DebugAI(AI_MSG_INFO, pSoldier, String("Climb roof at gridno %d", sClosestDisturbance), gLogDecideActionRed); return( AI_ACTION_CLIMB_ROOF ); } } @@ -4175,7 +4176,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) INT32 usClimbPoint = sClosestDisturbance; if (!TileIsOutOfBounds(usClimbPoint)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Move towards climb spot %d", usClimbPoint)); + DebugAI(AI_MSG_INFO, pSoldier, String("Move towards climb spot %d", usClimbPoint), gLogDecideActionRed); pSoldier->aiData.usActionData = usClimbPoint; return( AI_ACTION_MOVE_TO_CLIMB ); } @@ -4189,7 +4190,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) BOOLEAN fOvercrowded = FALSE; if( CountNearbyFriends(pSoldier, pSoldier->sGridNo, TACTICAL_RANGE / 4) > 2 ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Soldier position %d is overcrowded", pSoldier->sGridNo)); + DebugAI(AI_MSG_INFO, pSoldier, String("Soldier position %d is overcrowded", pSoldier->sGridNo), gLogDecideActionRed); fOvercrowded = TRUE; } @@ -4208,7 +4209,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->bActionPoints >= APBPConstants[AP_MINIMUM] && ( CountFriendsInDirection( pSoldier, sClosestDisturbance ) > 1 || NightTime() || fOvercrowded) ) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Possibly start flanking]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Possibly start flanking]"), gLogDecideActionRed); INT8 action = AI_ACTION_SEEK_OPPONENT; INT16 dist = PythSpacesAway ( pSoldier->sGridNo, sClosestDisturbance ); if ( dist > MIN_FLANK_DIST_RED && dist < MAX_FLANK_DIST_RED ) @@ -4222,35 +4223,35 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) case 3: if (pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT) { - DebugAI(AI_MSG_INFO, pSoldier, String("Try to flank left")); + DebugAI(AI_MSG_INFO, pSoldier, String("Try to flank left"), gLogDecideActionRed); action = AI_ACTION_FLANK_LEFT ; } break; default: if (pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT) { - DebugAI(AI_MSG_INFO, pSoldier, String("Try to flank right")); + DebugAI(AI_MSG_INFO, pSoldier, String("Try to flank right"), gLogDecideActionRed); action = AI_ACTION_FLANK_RIGHT ; } break; } if (action == AI_ACTION_SEEK_OPPONENT) { - DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy instead")); + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy instead"), gLogDecideActionRed); return action; } } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Distance not suitable, seek enemy instead")); - return AI_ACTION_SEEK_OPPONENT ; + DebugAI(AI_MSG_INFO, pSoldier, String("Distance not suitable, seek enemy instead"), gLogDecideActionRed); + return AI_ACTION_SEEK_OPPONENT; } pSoldier->aiData.usActionData = FindFlankingSpot (pSoldier, sClosestDisturbance, action ); if (TileIsOutOfBounds(pSoldier->aiData.usActionData) || pSoldier->numFlanks >= MAX_FLANKS_RED ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Flanking spot %d out of bounds or numFlanks >= MAX_FLANKS_RED", pSoldier->aiData.usActionData)); + DebugAI(AI_MSG_INFO, pSoldier, String("Flanking spot %d out of bounds or numFlanks >= MAX_FLANKS_RED", pSoldier->aiData.usActionData), gLogDecideActionRed); pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier,sClosestDisturbance,GetAPsCrouch( pSoldier, TRUE), AI_ACTION_SEEK_OPPONENT,0); //pSoldier->numFlanks = 0; if ( PythSpacesAway( pSoldier->aiData.usActionData, sClosestDisturbance ) < 5 || LocationToLocationLineOfSightTest( pSoldier->aiData.usActionData, pSoldier->pathing.bLevel, sClosestDisturbance, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS ) ) @@ -4260,7 +4261,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Reserved AP for crouch & shot, seek enemy")); + DebugAI(AI_MSG_INFO, pSoldier, String("Reserved AP for crouch & shot, seek enemy"), gLogDecideActionRed); pSoldier->aiData.fAIFlags |= AI_CAUTIOUS; pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; return(AI_ACTION_SEEK_OPPONENT); @@ -4268,13 +4269,13 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy")); + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy"), gLogDecideActionRed); return(AI_ACTION_SEEK_OPPONENT); } } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Found flanking spot %d", pSoldier->aiData.usActionData)); + DebugAI(AI_MSG_INFO, pSoldier, String("Found flanking spot %d", pSoldier->aiData.usActionData), gLogDecideActionRed); if ( action == AI_ACTION_FLANK_LEFT ) pSoldier->flags.lastFlankLeft = TRUE; else @@ -4294,13 +4295,13 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->aiData.bOrders = FARPATROL; } - DebugAI(AI_MSG_INFO, pSoldier, String("Start flanking")); + DebugAI(AI_MSG_INFO, pSoldier, String("Start flanking"), gLogDecideActionRed); return(action); } } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Not flanking, move up towards enemy")); + DebugAI(AI_MSG_INFO, pSoldier, String("Not flanking, move up towards enemy"), gLogDecideActionRed); // let's be a bit cautious about going right up to a location without enough APs to shoot if ( PythSpacesAway( pSoldier->aiData.usActionData, sClosestDisturbance ) < 5 || LocationToLocationLineOfSightTest( pSoldier->aiData.usActionData, pSoldier->pathing.bLevel, sClosestDisturbance, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS ) ) { @@ -4309,7 +4310,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Reserved AP for crouch & shot, seek enemy")); + DebugAI(AI_MSG_INFO, pSoldier, String("Reserved AP for crouch & shot, seek enemy"), gLogDecideActionRed); pSoldier->aiData.fAIFlags |= AI_CAUTIOUS; pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; return(AI_ACTION_SEEK_OPPONENT); @@ -4317,7 +4318,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy")); + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy"), gLogDecideActionRed); return(AI_ACTION_SEEK_OPPONENT); } break; @@ -4337,7 +4338,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if WATCHING is possible and at least as desirable as anything else if ((bWatchPts > -90) && (bWatchPts >= bSeekPts) && (bWatchPts >= bHelpPts) && (bWatchPts >= bHidePts )) { - DebugAI(AI_MSG_INFO, pSoldier, String("[watch]")); + DebugAI(AI_MSG_INFO, pSoldier, String("[watch]"), gLogDecideActionRed); // take a look at our highest watch point... if it's still visible, turn to face it and then wait bHighestWatchLoc = GetHighestVisibleWatchedLoc( pSoldier->ubID ); @@ -4345,7 +4346,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { // see if we need turn to face that location ubOpponentDir = AIDirection(pSoldier->sGridNo, gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc]); - DebugAI(AI_MSG_INFO, pSoldier, String("Highest watch location: [%d] %d %d watch dir: %d", bHighestWatchLoc, gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc], gbWatchedLocLevel[pSoldier->ubID][bHighestWatchLoc], ubOpponentDir)); + DebugAI(AI_MSG_INFO, pSoldier, String("Highest watch location: [%d] %d %d watch dir: %d", bHighestWatchLoc, gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc], gbWatchedLocLevel[pSoldier->ubID][bHighestWatchLoc], ubOpponentDir), gLogDecideActionRed); // consider at least crouching if (gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_STAND && @@ -4354,7 +4355,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { pSoldier->aiData.usActionData = ANIM_CROUCH; - DebugAI(AI_MSG_INFO, pSoldier, String("crouch to watch")); + DebugAI(AI_MSG_INFO, pSoldier, String("crouch to watch"), gLogDecideActionRed); return(AI_ACTION_CHANGE_STANCE); } @@ -4364,7 +4365,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) (pSoldier->bBreath > OKBREATH * 2 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 50) && pSoldier->bActionPoints >= GetAPsToReadyWeapon(pSoldier, PickSoldierReadyAnimation(pSoldier, FALSE, FALSE))) { - DebugAI(AI_MSG_INFO, pSoldier, String("raise weapon")); + DebugAI(AI_MSG_INFO, pSoldier, String("raise weapon"), gLogDecideActionRed); return AI_ACTION_RAISE_GUN; } @@ -4375,7 +4376,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { // turn pSoldier->aiData.usActionData = ubOpponentDir; - DebugAI(AI_MSG_INFO, pSoldier, String("turn to watched location")); + DebugAI(AI_MSG_INFO, pSoldier, String("turn to watched location"), gLogDecideActionRed); return(AI_ACTION_CHANGE_FACING); } @@ -4389,11 +4390,11 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { pSoldier->aiData.usActionData = ANIM_PRONE; pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; - DebugAI(AI_MSG_INFO, pSoldier, String("go prone, end turn")); + DebugAI(AI_MSG_INFO, pSoldier, String("go prone, end turn"), gLogDecideActionRed); return(AI_ACTION_CHANGE_STANCE); } - DebugAI(AI_MSG_INFO, pSoldier, String("watch at %d level %d", gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc], gbWatchedLocLevel[pSoldier->ubID][bHighestWatchLoc])); + DebugAI(AI_MSG_INFO, pSoldier, String("watch at %d level %d", gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc], gbWatchedLocLevel[pSoldier->ubID][bHighestWatchLoc]), gLogDecideActionRed); return(AI_ACTION_NONE); } @@ -4406,7 +4407,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if HELPING is possible and at least as desirable as seeking or hiding if ((bHelpPts > -90) && (bHelpPts >= bSeekPts) && (bHelpPts >= bHidePts) && (bHelpPts >= bWatchPts )) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Help a friend]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Help a friend]"), gLogDecideActionRed); #ifdef AI_TIMING_TESTS uiStartTime = GetJA2Clock(); #endif @@ -4422,7 +4423,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //if (!TileIsOutOfBounds(sClosestFriend) && PythSpacesAway(pSoldier->sGridNo, sClosestFriend) > pSoldier->GetMaxDistanceVisible(sClosestFriend, 0, CALC_FROM_ALL_DIRS )) if (!TileIsOutOfBounds(sClosestFriend)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Closest friend at gridno %d", sClosestFriend)); + DebugAI(AI_MSG_INFO, pSoldier, String("Closest friend at gridno %d", sClosestFriend), gLogDecideActionRed); ////////////////////////////////////////////////////////////////////// // GO DIRECTLY TOWARDS CLOSEST FRIEND UNDER FIRE OR WHO LAST RADIOED ////////////////////////////////////////////////////////////////////// @@ -4435,7 +4436,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->name,sClosestFriend,pSoldier->aiData.usActionData); AIPopMessage(tempstr); #endif - DebugAI(AI_MSG_INFO, pSoldier, String("Seeking friend, moving to %d", pSoldier->aiData.usActionData)); + DebugAI(AI_MSG_INFO, pSoldier, String("Seeking friend, moving to %d", pSoldier->aiData.usActionData), gLogDecideActionRed); if ( !ENEMYROBOT(pSoldier) && fClimb )//&& pSoldier->aiData.usActionData == sClosestFriend) { @@ -4454,7 +4455,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if (IsActionAffordable(pSoldier) && pSoldier->bActionPoints >= ( APBPConstants[AP_CLIMBROOF] + MinAPsToAttack( pSoldier, sClosestFriend, ADDTURNCOST,0))) { - DebugAI(AI_MSG_INFO, pSoldier, String("Climb roof")); + DebugAI(AI_MSG_INFO, pSoldier, String("Climb roof"), gLogDecideActionRed); return( AI_ACTION_CLIMB_ROOF ); } } @@ -4465,7 +4466,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //if (!TileIsOutOfBounds(sClimbPoint)) { //pSoldier->aiData.usActionData = sClimbPoint; - DebugAI(AI_MSG_INFO, pSoldier, String("Move towards climb point")); + DebugAI(AI_MSG_INFO, pSoldier, String("Move towards climb point"), gLogDecideActionRed); return( AI_ACTION_MOVE_TO_CLIMB ); } } @@ -4474,7 +4475,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //{ // return( AI_ACTION_CLIMB_ROOF ); //} - DebugAI(AI_MSG_INFO, pSoldier, String("Seek friend")); + DebugAI(AI_MSG_INFO, pSoldier, String("Seek friend"), gLogDecideActionRed); return(AI_ACTION_SEEK_FRIEND); } } @@ -4493,7 +4494,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if HIDING is possible and at least as desirable as seeking or helping if ((bHidePts > -90) && (bHidePts >= bSeekPts) && (bHidePts >= bHelpPts) && (bHidePts >= bWatchPts )) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Take cover]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Take cover]"), gLogDecideActionRed); //sClosestOpponent = ClosestKnownOpponent( pSoldier, NULL, NULL ); // if an opponent is known (not necessarily reachable or conscious) if (!SkipCoverCheck && !TileIsOutOfBounds(sClosestOpponent)) @@ -4516,7 +4517,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // let's be a bit cautious about going right up to a location without enough APs to shoot if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Found a cover spot at %d", pSoldier->aiData.usActionData)); + DebugAI(AI_MSG_INFO, pSoldier, String("Found a cover spot at %d", pSoldier->aiData.usActionData), gLogDecideActionRed); sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb); if (!TileIsOutOfBounds(sClosestDisturbance) && ( SpacesAway( pSoldier->aiData.usActionData, sClosestDisturbance ) < 5 || SpacesAway( pSoldier->aiData.usActionData, sClosestDisturbance ) + 5 < SpacesAway( pSoldier->sGridNo, sClosestDisturbance ) ) ) { @@ -4524,13 +4525,13 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // ensure will we have enough APs for a possible crouch plus a shot if ( InternalGoAsFarAsPossibleTowards( pSoldier, pSoldier->aiData.usActionData, (INT8) (MinAPsToAttack( pSoldier, sClosestOpponent, ADDTURNCOST,0) + GetAPsCrouch( pSoldier, TRUE)), AI_ACTION_TAKE_COVER, 0 ) == pSoldier->aiData.usActionData ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Moving to cover, reserve AP for crouch & shot")); + DebugAI(AI_MSG_INFO, pSoldier, String("Moving to cover, reserve AP for crouch & shot"), gLogDecideActionRed); return(AI_ACTION_TAKE_COVER); } } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Moving to cover")); + DebugAI(AI_MSG_INFO, pSoldier, String("Moving to cover"), gLogDecideActionRed); return(AI_ACTION_TAKE_COVER); } } @@ -4555,7 +4556,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if we're currently under fire (presumably, attacker is hidden) if (pSoldier->aiData.bUnderFire) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Under fire]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Under fire]"), gLogDecideActionRed); // only try to run if we've actually been hit recently & noticably so // otherwise, presumably our current cover is pretty good & sufficient @@ -4577,7 +4578,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (bShock > 0) { - DebugAI(AI_MSG_INFO, pSoldier, String("Soldier is shocked, attempt to run away")); + DebugAI(AI_MSG_INFO, pSoldier, String("Soldier is shocked, attempt to run away"), gLogDecideActionRed); // look for best place to RUN AWAY to (farthest from the closest threat) pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); @@ -4589,7 +4590,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) #endif DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: run away!"); - DebugAI(AI_MSG_INFO, pSoldier, String("Running away to gridno %d", pSoldier->aiData.usActionData)); + DebugAI(AI_MSG_INFO, pSoldier, String("Running away to gridno %d", pSoldier->aiData.usActionData), gLogDecideActionRed); return(AI_ACTION_RUN_AWAY); } } @@ -4598,7 +4599,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // UNDER FIRE, DON'T WANNA/CAN'T RUN AWAY, SO CROUCH //////////////////////////////////////////////////////////////////////////// - DebugAI(AI_MSG_INFO, pSoldier, String("Under fire, try to change stance")); + DebugAI(AI_MSG_INFO, pSoldier, String("Under fire, try to change stance"), gLogDecideActionRed); DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: crouch or go prone"); // if not in water and not already crouched if (gAnimControl[pSoldier->usAnimState].ubHeight == ANIM_STAND && IsValidStance(pSoldier, ANIM_CROUCH)) @@ -4610,7 +4611,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) sprintf(tempstr, "%s CROUCHES (STATUS RED)", pSoldier->name); AIPopMessage(tempstr); #endif - DebugAI(AI_MSG_INFO, pSoldier, String("Crouching")); + DebugAI(AI_MSG_INFO, pSoldier, String("Crouching"), gLogDecideActionRed); pSoldier->aiData.usActionData = ANIM_CROUCH; return(AI_ACTION_CHANGE_STANCE); @@ -4621,7 +4622,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // maybe go prone if (PreRandom(2) == 0 && IsValidStance(pSoldier, ANIM_PRONE)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Go prone")); + DebugAI(AI_MSG_INFO, pSoldier, String("Go prone"), gLogDecideActionRed); pSoldier->aiData.usActionData = ANIM_PRONE; return(AI_ACTION_CHANGE_STANCE); } @@ -4637,27 +4638,27 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //!TileIsOutOfBounds( sClosestNoise ) && PythSpacesAway(pSoldier->sGridNo, sClosestNoise) < TACTICAL_RANGE / 2) ) //CorpseWarning(pSoldier, pSoldier->sGridNo, pSoldier->pathing.bLevel) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[civilians run away]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[civilians run away]"), gLogDecideActionRed); // look for best place to RUN AWAY to (farthest from the closest threat) pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); - DebugAI(AI_MSG_INFO, pSoldier, String("found run away spot %d", pSoldier->aiData.usActionData)); + DebugAI(AI_MSG_INFO, pSoldier, String("found run away spot %d", pSoldier->aiData.usActionData), gLogDecideActionRed); if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Running away!")); + DebugAI(AI_MSG_INFO, pSoldier, String("Running away!"), gLogDecideActionRed); return(AI_ACTION_RUN_AWAY); } //else if (!pSoldier->SkipCoverCheck() && gfTurnBasedAI) // only do in turnbased else if (!SkipCoverCheck && gfTurnBasedAI) // only do in turnbased { - DebugAI(AI_MSG_INFO, pSoldier, String("Can't run away, try to take cover")); + DebugAI(AI_MSG_INFO, pSoldier, String("Can't run away, try to take cover"), gLogDecideActionRed); // try to take cover pSoldier->aiData.bAIMorale = MORALE_WORRIED; pSoldier->aiData.usActionData = FindBestNearbyCover(pSoldier, MORALE_WORRIED, &iDummy); if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Take cover")); + DebugAI(AI_MSG_INFO, pSoldier, String("Take cover"), gLogDecideActionRed); return(AI_ACTION_TAKE_COVER); } } @@ -4676,7 +4677,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!TileIsOutOfBounds(sClosestOpponent)) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Look around towards enemy]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Look around towards enemy]"), gLogDecideActionRed); // determine direction from this soldier to the closest opponent ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestOpponent); @@ -4709,7 +4710,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) sprintf(tempstr,"%s - TURNS TOWARDS CLOSEST ENEMY to face direction %d",pSoldier->name,pSoldier->aiData.usActionData); AIPopMessage(tempstr); #endif - DebugAI(AI_MSG_INFO, pSoldier, String("Turn towards closest enemy, face direction %d", pSoldier->aiData.usActionData)); + DebugAI(AI_MSG_INFO, pSoldier, String("Turn towards closest enemy, face direction %d", pSoldier->aiData.usActionData), gLogDecideActionRed); if ( pSoldier->aiData.bOrders == SNIPER && !WeaponReady(pSoldier) && PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION && @@ -4717,7 +4718,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if (!gfTurnBasedAI || GetAPsToReadyWeapon( pSoldier, READY_RIFLE_CROUCH ) <= pSoldier->bActionPoints) { - DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, sniper")); + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, sniper"), gLogDecideActionRed); pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; } } @@ -4733,7 +4734,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if ( Random(100) < 35 ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, scoped weapon")); + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, scoped weapon"), gLogDecideActionRed); pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; } } @@ -4749,19 +4750,19 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) !WeaponReady(pSoldier) && PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) { - DebugAI(AI_MSG_INFO, pSoldier, String("Facing enemy already")); + DebugAI(AI_MSG_INFO, pSoldier, String("Facing enemy already"), gLogDecideActionRed); if ((!gfTurnBasedAI || GetAPsToReadyWeapon( pSoldier, pSoldier->usAnimState ) <= pSoldier->bActionPoints) && (pSoldier->bBreath > 15 || GetBPCostPer10APsForGunHolding( pSoldier, TRUE ) < 50)) { if ( pSoldier->aiData.bOrders == SNIPER ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, sniper")); + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, sniper"), gLogDecideActionRed); return AI_ACTION_RAISE_GUN; } else if (IsScoped(&pSoldier->inv[HANDPOS])) { if ( Random(100) < 40 ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, scoped weapon")); + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, scoped weapon"), gLogDecideActionRed); return AI_ACTION_RAISE_GUN; } } @@ -4769,7 +4770,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if ( Random(100) < 20 ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun")); + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun"), gLogDecideActionRed); return AI_ACTION_RAISE_GUN; } } @@ -4781,7 +4782,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( ARMED_VEHICLE( pSoldier ) ) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Armed vehicle]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Armed vehicle]"), gLogDecideActionRed); // try turning in a random direction as we still can't see anyone. if (!gfTurnBasedAI || GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints) { @@ -4792,13 +4793,13 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestDisturbance); if ( pSoldier->ubDirection == ubOpponentDir ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Already facing closest disturbance, face a random direction")); + DebugAI(AI_MSG_INFO, pSoldier, String("Already facing closest disturbance, face a random direction"), gLogDecideActionRed); ubOpponentDir = (UINT8) PreRandom( NUM_WORLD_DIRECTIONS ); } } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Closest disturbance out of bounds, face a random direction")); + DebugAI(AI_MSG_INFO, pSoldier, String("Closest disturbance out of bounds, face a random direction"), gLogDecideActionRed); ubOpponentDir = (UINT8) PreRandom( NUM_WORLD_DIRECTIONS ); } @@ -4818,7 +4819,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if ( gfTurnBasedAI ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Ending turn to limit facing changes")); + DebugAI(AI_MSG_INFO, pSoldier, String("Ending turn to limit facing changes"), gLogDecideActionRed); pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; } else @@ -4828,14 +4829,14 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } - DebugAI(AI_MSG_INFO, pSoldier, String("Turn towards closest disturbance, direction %d", pSoldier->aiData.usActionData)); + DebugAI(AI_MSG_INFO, pSoldier, String("Turn towards closest disturbance, direction %d", pSoldier->aiData.usActionData), gLogDecideActionRed); return(AI_ACTION_CHANGE_FACING); } } } // that's it for tanks - DebugAI(AI_MSG_INFO, pSoldier, String("Do nothing")); + DebugAI(AI_MSG_INFO, pSoldier, String("Do nothing"), gLogDecideActionRed); return( AI_ACTION_NONE ); } @@ -4863,7 +4864,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) CountFriendsBlack(pSoldier) == 0 ) { // abort! abort! - DebugAI(AI_MSG_INFO, pSoldier, String("Unsafe location, do nothing")); + DebugAI(AI_MSG_INFO, pSoldier, String("Unsafe location, do nothing"), gLogDecideActionRed); pSoldier->aiData.bAction = AI_ACTION_NONE; } @@ -4920,7 +4921,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // if not in water and not already crouched, try to crouch down first if (!fCivilian && !bInWater && (gAnimControl[ pSoldier->usAnimState ].ubHeight == ANIM_STAND) && IsValidStance( pSoldier, ANIM_CROUCH ) ) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Crouch]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Crouch]"), gLogDecideActionRed); //sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); //if ( ( !TileIsOutOfBounds(sClosestOpponent) && PythSpacesAway( pSoldier->sGridNo, sClosestOpponent ) < (MaxNormalDistanceVisible() * 3) / 2 ) || PreRandom( 4 ) == 0 ) @@ -4949,7 +4950,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if ( Random(100) < 40 ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, scoped weapon")); + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, scoped weapon"), gLogDecideActionRed); pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; } } @@ -4958,7 +4959,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //////////////////////////////////////////////////////////////////////////// - DebugAI(AI_MSG_INFO, pSoldier, String("Change stance to crouch")); + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance to crouch"), gLogDecideActionRed); pSoldier->aiData.usActionData = ANIM_CROUCH; return(AI_ACTION_CHANGE_STANCE); } @@ -4971,7 +4972,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( !fCivilian && pSoldier->aiData.bUnderFire && pSoldier->bActionPoints >= (pSoldier->bInitialActionPoints - GetAPsToLook( pSoldier ) ) && IsValidStance( pSoldier, ANIM_PRONE ) ) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Under fire, go prone]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Under fire, go prone]"), gLogDecideActionRed); sClosestDisturbance = MostImportantNoiseHeard( pSoldier, NULL, NULL, NULL ); if (!TileIsOutOfBounds(sClosestDisturbance)) @@ -4981,7 +4982,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if ( !gfTurnBasedAI || GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Face direction %d", ubOpponentDir)); + DebugAI(AI_MSG_INFO, pSoldier, String("Face direction %d", ubOpponentDir), gLogDecideActionRed); pSoldier->aiData.usActionData = ubOpponentDir; return( AI_ACTION_CHANGE_FACING ); } @@ -4989,7 +4990,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) else if ( (!gfTurnBasedAI || GetAPsToChangeStance( pSoldier, ANIM_PRONE ) <= pSoldier->bActionPoints ) && pSoldier->InternalIsValidStance( ubOpponentDir, ANIM_PRONE ) ) { // go prone, end turn - DebugAI(AI_MSG_INFO, pSoldier, String("Go prone & end turn")); + DebugAI(AI_MSG_INFO, pSoldier, String("Go prone & end turn"), gLogDecideActionRed); pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; pSoldier->aiData.usActionData = ANIM_PRONE; return( AI_ACTION_CHANGE_STANCE ); @@ -5003,7 +5004,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //////////////////////////////////////////////////////////////////////////// if ( pSoldier->aiData.bOrders == SNIPER ) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Sniper]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Sniper]"), gLogDecideActionRed); if ( pSoldier->sniper == 0 ) { DebugMsg(TOPIC_JA2,DBG_LEVEL_3,String("DecideActionRed: sniper raising gun...")); @@ -5012,7 +5013,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (!WeaponReady(pSoldier) && PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) { - DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, sniper")); + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, sniper"), gLogDecideActionRed); pSoldier->sniper = 1; return AI_ACTION_RAISE_GUN; } @@ -5020,7 +5021,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } else { - DebugAI(AI_MSG_INFO, pSoldier, String("Switch to yellow state")); + DebugAI(AI_MSG_INFO, pSoldier, String("Switch to yellow state"), gLogDecideActionRed); pSoldier->sniper = 0; return(DecideActionYellow(pSoldier)); } @@ -5039,7 +5040,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { if ( Random(100) < 35 ) { - DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun")); + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun"), gLogDecideActionRed); return( AI_ACTION_RAISE_GUN ); } } @@ -5056,7 +5057,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) #ifdef DEBUGDECISIONS AINameMessage(pSoldier,"- DOES NOTHING (RED)",1000); #endif - DebugAI(AI_MSG_INFO, pSoldier, String("Do nothing")); + DebugAI(AI_MSG_INFO, pSoldier, String("Do nothing"), gLogDecideActionRed); pSoldier->aiData.usActionData = NOWHERE; return(AI_ACTION_NONE); @@ -10504,21 +10505,21 @@ extern UINT32 guiTurnCnt; extern UINT32 guiReinforceTurn; extern UINT32 guiArrived; -void LogDecideInfo(SOLDIERTYPE *pSoldier) +void LogDecideInfo(SOLDIERTYPE *pSoldier, bool doLog) { - DebugAI(AI_MSG_INFO, pSoldier, String("Turn num %d aware %d ubID %d", guiTurnCnt, gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition, pSoldier->ubID)); - DebugAI(AI_MSG_INFO, pSoldier, String("current team %d interrupt occurred %d", gTacticalStatus.ubCurrentTeam, gTacticalStatus.fInterruptOccurred)); - DebugAI(AI_MSG_INFO, pSoldier, String("AP=%d/%d %s %s %s %s %s", pSoldier->bActionPoints, pSoldier->bInitialActionPoints, gStr8AlertStatus[pSoldier->aiData.bAlertStatus], gStr8Orders[pSoldier->aiData.bOrders], gStr8Attitude[pSoldier->aiData.bAttitude], gStr8Team[pSoldier->bTeam], gStr8Class[pSoldier->ubSoldierClass])); - DebugAI(AI_MSG_INFO, pSoldier, String("Health %d/%d Breath %d/%d Shock %d Tolerance %d AI Morale %d Morale %d", pSoldier->stats.bLife, pSoldier->stats.bLifeMax, pSoldier->bBreath, pSoldier->bBreathMax, pSoldier->aiData.bShock, CalcSuppressionTolerance(pSoldier), pSoldier->aiData.bAIMorale, pSoldier->aiData.bMorale)); - DebugAI(AI_MSG_INFO, pSoldier, String("Spot %d level %d opponents %d", pSoldier->sGridNo, pSoldier->pathing.bLevel, pSoldier->aiData.bOppCnt)); - DebugAI(AI_MSG_INFO, pSoldier, String("ubServiceCount %d ubServicePartner %d fDoingSurgery %d", pSoldier->ubServiceCount, pSoldier->ubServicePartner, pSoldier->fDoingSurgery)); + DebugAI(AI_MSG_INFO, pSoldier, String("Turn num %d aware %d", guiTurnCnt, gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition), doLog); + DebugAI(AI_MSG_INFO, pSoldier, String("current team %d interrupt occurred %d", gTacticalStatus.ubCurrentTeam, gTacticalStatus.fInterruptOccurred), doLog); + DebugAI(AI_MSG_INFO, pSoldier, String("AP=%d/%d %s %s %s %s %s", pSoldier->bActionPoints, pSoldier->bInitialActionPoints, gStr8AlertStatus[pSoldier->aiData.bAlertStatus], gStr8Orders[pSoldier->aiData.bOrders], gStr8Attitude[pSoldier->aiData.bAttitude], gStr8Team[pSoldier->bTeam], gStr8Class[pSoldier->ubSoldierClass]), doLog); + DebugAI(AI_MSG_INFO, pSoldier, String("Health %d/%d Breath %d/%d Shock %d Tolerance %d AI Morale %d Morale %d", pSoldier->stats.bLife, pSoldier->stats.bLifeMax, pSoldier->bBreath, pSoldier->bBreathMax, pSoldier->aiData.bShock, CalcSuppressionTolerance(pSoldier), pSoldier->aiData.bAIMorale, pSoldier->aiData.bMorale), doLog); + DebugAI(AI_MSG_INFO, pSoldier, String("Spot %d level %d opponents %d", pSoldier->sGridNo, pSoldier->pathing.bLevel, pSoldier->aiData.bOppCnt), doLog); + DebugAI(AI_MSG_INFO, pSoldier, String("ubServiceCount %d ubServicePartner %d fDoingSurgery %d", pSoldier->ubServiceCount, pSoldier->ubServicePartner, pSoldier->fDoingSurgery), doLog); if (pSoldier->IsCowering()) { - DebugAI(AI_MSG_INFO, pSoldier, String("Cowering")); + DebugAI(AI_MSG_INFO, pSoldier, String("Cowering"), doLog); } if (pSoldier->IsGivingAid()) { - DebugAI(AI_MSG_INFO, pSoldier, String("Giving aid")); + DebugAI(AI_MSG_INFO, pSoldier, String("Giving aid"), doLog); } //CHAR8 str8[1024]; @@ -10528,18 +10529,18 @@ void LogDecideInfo(SOLDIERTYPE *pSoldier) { if (!TileIsOutOfBounds(gsWatchedLoc[pSoldier->ubID][bLoop])) { - DebugAI(AI_MSG_INFO, pSoldier, String("Watched location %d level %d points %d", gsWatchedLoc[pSoldier->ubID][bLoop], gbWatchedLocLevel[pSoldier->ubID][bLoop], gubWatchedLocPoints[pSoldier->ubID][bLoop])); + DebugAI(AI_MSG_INFO, pSoldier, String("Watched location %d level %d points %d", gsWatchedLoc[pSoldier->ubID][bLoop], gbWatchedLocLevel[pSoldier->ubID][bLoop], gubWatchedLocPoints[pSoldier->ubID][bLoop]), doLog); } } - LogKnowledgeInfo(pSoldier); + LogKnowledgeInfo(pSoldier, doLog); - DebugAI(AI_MSG_INFO, pSoldier, String("What I know %d", WhatIKnowThatPublicDont(pSoldier, FALSE))); - DebugAI(AI_MSG_INFO, pSoldier, String("Has Gun %d, Short range weapon %d, Gun Range %d, Gun Ammo %d, Gun Scoped %d ", AICheckHasGun(pSoldier), AICheckShortWeaponRange(pSoldier), AIGunRange(pSoldier), AIGunAmmo(pSoldier), AIGunScoped(pSoldier))); - DebugAI(AI_MSG_INFO, pSoldier, String("RetreatCounter %d", pSoldier->RetreatCounterValue())); + DebugAI(AI_MSG_INFO, pSoldier, String("What I know %d", WhatIKnowThatPublicDont(pSoldier, FALSE)), doLog); + DebugAI(AI_MSG_INFO, pSoldier, String("Has Gun %d, Short range weapon %d, Gun Range %d, Gun Ammo %d, Gun Scoped %d ", AICheckHasGun(pSoldier), AICheckShortWeaponRange(pSoldier), AIGunRange(pSoldier), AIGunAmmo(pSoldier), AIGunScoped(pSoldier)), doLog); + DebugAI(AI_MSG_INFO, pSoldier, String("RetreatCounter %d", pSoldier->RetreatCounterValue()), doLog); } -void LogKnowledgeInfo(SOLDIERTYPE *pSoldier) +void LogKnowledgeInfo(SOLDIERTYPE *pSoldier, bool doLog) { //CHAR8 str8[1024]; //memset(str8, 0, 1024 * sizeof(char)); @@ -10552,7 +10553,7 @@ void LogKnowledgeInfo(SOLDIERTYPE *pSoldier) { //wcstombs(str8, MercPtrs[oppID]->GetName(), wcslen(MercPtrs[oppID]->GetName())+1); //wcstombs(str8, MercPtrs[oppID]->GetName(), 1024 - 1); - DebugAI(AI_MSG_INFO, pSoldier, String("public opponent [%d] knowledge %s gridno %d level %d", oppID, gStr8Knowledge[gbPublicOpplist[pSoldier->bTeam][oppID] - OLDEST_HEARD_VALUE], gsPublicLastKnownOppLoc[pSoldier->bTeam][oppID], gbPublicLastKnownOppLevel[pSoldier->bTeam][oppID])); + DebugAI(AI_MSG_INFO, pSoldier, String("public opponent [%d] knowledge %s gridno %d level %d", oppID, gStr8Knowledge[gbPublicOpplist[pSoldier->bTeam][oppID] - OLDEST_HEARD_VALUE], gsPublicLastKnownOppLoc[pSoldier->bTeam][oppID], gbPublicLastKnownOppLevel[pSoldier->bTeam][oppID]), doLog); //swprintf( pStrInfo, L"%s[%d] %s %s\n", pStrInfo, oppID, MercPtrs[oppID]->GetName(), SeenStr(gbPublicOpplist[pSoldier->bTeam][oppID]) ); } } @@ -10564,7 +10565,7 @@ void LogKnowledgeInfo(SOLDIERTYPE *pSoldier) { //wcstombs(str8, MercPtrs[oppID]->GetName(), wcslen(MercPtrs[oppID]->GetName())+1); //wcstombs(str8, MercPtrs[oppID]->GetName(), 1024 - 1); - DebugAI(AI_MSG_INFO, pSoldier, String("personal opponent [%d] knowledge %s gridno %d level %d", oppID, gStr8Knowledge[pSoldier->aiData.bOppList[oppID] - OLDEST_HEARD_VALUE], gsLastKnownOppLoc[pSoldier->ubID][oppID], gbLastKnownOppLevel[pSoldier->ubID][oppID])); + DebugAI(AI_MSG_INFO, pSoldier, String("personal opponent [%d] knowledge %s gridno %d level %d", oppID, gStr8Knowledge[pSoldier->aiData.bOppList[oppID] - OLDEST_HEARD_VALUE], gsLastKnownOppLoc[pSoldier->ubID][oppID], gbLastKnownOppLevel[pSoldier->ubID][oppID]), doLog); //swprintf( pStrInfo, L"%s[%d] %s %s\n", pStrInfo, oppID, MercPtrs[oppID]->GetName(), SeenStr(pSoldier->aiData.bOppList[oppID]) ); } } diff --git a/TacticalAI/ZombieDecideAction.cpp b/TacticalAI/ZombieDecideAction.cpp index 16c7f321f..ee917bea1 100644 --- a/TacticalAI/ZombieDecideAction.cpp +++ b/TacticalAI/ZombieDecideAction.cpp @@ -33,7 +33,7 @@ #include "Text.h" extern BOOLEAN gfHiddenInterrupt; -extern void LogDecideInfo(SOLDIERTYPE *pSoldier); +extern void LogDecideInfo(SOLDIERTYPE *pSoldier, bool doLog = true); extern STR8 gStr8AlertStatus[]; extern STR8 gStr8Attitude[]; diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index c97d19362..cd25caf57 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -182,7 +182,7 @@ BOOLEAN CanAutoBandage( BOOLEAN fDoFullCheck ); void DebugAI( STR szOutput ); enum { AI_MSG_START, AI_MSG_DECIDE, AI_MSG_INFO, AI_MSG_TOPIC }; -void DebugAI(INT8 bMsgType, SOLDIERTYPE *pSoldier, STR szOutput, INT8 bAction = -1); +void DebugAI(INT8 bMsgType, SOLDIERTYPE *pSoldier, STR szOutput, bool doLog = true, INT8 bAction = -1); void DebugQuestInfo(STR szOutput); INT8 DecideAction(SOLDIERTYPE *pSoldier); INT8 DecideActionBlack(SOLDIERTYPE *pSoldier); From cd193c7f5ee0e29fb10bd4a447934dc3d9b58d8f Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 4 Feb 2024 16:07:03 +0200 Subject: [PATCH 28/80] Call TurnBasedHandleNPCAI() if no action is in progress Several actions would sometimes get stuck in a forever loop, where they would not be executed, since no proper path for executing the actions is found. Originally TurnBasedHandleNPCAI() would only be called if aidata.bAction was AI_ACTION_NONE. Now we'll call it if an action is not already in progress, and will be executed if the action is affordable. --- TacticalAI/AIMain.cpp | 54 +++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 0e78ef590..1aa88ee3f 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -859,7 +859,7 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named return; } - if (pSoldier->aiData.bAction == AI_ACTION_NONE) + if (!pSoldier->aiData.bActionInProgress) { // being handled so turn off muzzle flash if ( pSoldier->flags.fMuzzleFlash ) @@ -1837,41 +1837,41 @@ void TurnBasedHandleNPCAI(SOLDIERTYPE *pSoldier) { pSoldier->aiData.bAction = AI_ACTION_NONE; } + } - // if he chose to continue doing nothing - if (pSoldier->aiData.bAction == AI_ACTION_NONE) - { + // if he chose to continue doing nothing + if (pSoldier->aiData.bAction == AI_ACTION_NONE) + { #ifdef RECORDNET - fprintf(NetDebugFile,"\tMOVED BECOMING TRUE: Chose to do nothing, guynum %d\n",pSoldier->ubID); + fprintf(NetDebugFile,"\tMOVED BECOMING TRUE: Chose to do nothing, guynum %d\n",pSoldier->ubID); #endif - DebugMsg (TOPIC_JA2AI,DBG_LEVEL_3,"NPC has no action assigned"); - NPCDoesNothing(pSoldier); // sets pSoldier->moved to TRUE - return; - } - // to get here, we MUST have an action selected, but not in progress... - // see if we can afford to do this action - if (IsActionAffordable(pSoldier)) - { - NPCDoesAct(pSoldier); + DebugMsg (TOPIC_JA2AI,DBG_LEVEL_3,"NPC has no action assigned"); + NPCDoesNothing(pSoldier); // sets pSoldier->moved to TRUE + return; + } + // to get here, we MUST have an action selected, but not in progress... + // see if we can afford to do this action + if (IsActionAffordable(pSoldier)) + { + NPCDoesAct(pSoldier); - // perform the chosen action - pSoldier->aiData.bActionInProgress = ExecuteAction(pSoldier); // if started, mark us as busy + // perform the chosen action + pSoldier->aiData.bActionInProgress = ExecuteAction(pSoldier); // if started, mark us as busy - if ( !pSoldier->aiData.bActionInProgress && !TileIsOutOfBounds(pSoldier->sAbsoluteFinalDestination)) - { - // turn based... abort this guy's turn - EndAIGuysTurn( pSoldier ); - } - } - else + if ( !pSoldier->aiData.bActionInProgress && !TileIsOutOfBounds(pSoldier->sAbsoluteFinalDestination)) { + // turn based... abort this guy's turn + EndAIGuysTurn( pSoldier ); + } + } + else + { #ifdef DEBUGDECISIONS - AINumMessage("HandleManAI - Not enough APs, skipping guy#",pSoldier->ubID); + AINumMessage("HandleManAI - Not enough APs, skipping guy#",pSoldier->ubID); #endif - HaltMoveForSoldierOutOfPoints( pSoldier); - return; - } + HaltMoveForSoldierOutOfPoints( pSoldier); + return; } } From 01327265a3c59fc0e26aa9b269e831ded2037c01 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 4 Feb 2024 16:10:26 +0200 Subject: [PATCH 29/80] Check if action should stay inprogress state Several actions tended to get stuck in an invalid state, where it was completed, but aiData.bActionInProgress was still TRUE resulting in an AI deadlock. If such a situation is found, Actiondone is now called, preventing the deadlock --- TacticalAI/AIMain.cpp | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 1aa88ee3f..ff85945f3 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -440,6 +440,27 @@ void DebugQuestInfo(STR szOutput) } } +static INT16 ShouldActionStayInProgress(SOLDIERTYPE* pSoldier) +{ + // this here should never happen, but it seems to (turns sometimes hang!) + if ((pSoldier->aiData.bAction == AI_ACTION_CHANGE_FACING) && (pSoldier->pathing.bDesiredDirection != pSoldier->aiData.usActionData)) + { + // don't try to pay any more APs for this, it was paid for once already! + pSoldier->pathing.bDesiredDirection = (INT8)pSoldier->aiData.usActionData; // turn to face direction in actionData + return(TRUE); + } + else if ((pSoldier->aiData.bAction == AI_ACTION_CHANGE_FACING) && (pSoldier->pathing.bDesiredDirection == pSoldier->aiData.usActionData)) + { + return(FALSE); + } + else if (pSoldier->aiData.bAction == AI_ACTION_END_TURN || pSoldier->aiData.bAction == AI_ACTION_NONE) + { + return(FALSE); + } + // needs more time to complete action + return(TRUE); +} + BOOLEAN InitAI( void ) { @@ -992,6 +1013,12 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named { ActionDone(pSoldier); } + + if (!ShouldActionStayInProgress(pSoldier)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Action % s was stuck as being in progress. Canceling action", wszAction[pSoldier->aiData.bAction])); + ActionDone(pSoldier); + } } /********* End of new overall AI system @@ -1460,7 +1487,7 @@ void FreeUpNPCFromRoofClimb(SOLDIERTYPE *pSoldier ) void ActionDone(SOLDIERTYPE *pSoldier) { // if an action is currently selected - if (pSoldier->aiData.bAction != AI_ACTION_NONE) + //if (pSoldier->aiData.bAction != AI_ACTION_NONE) { if (pSoldier->flags.uiStatusFlags & SOLDIER_MONSTER) { From f646bc2bfa636fc0d9ee5f2302070b5d94d44977 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 4 Feb 2024 16:14:01 +0200 Subject: [PATCH 30/80] Set correct next action if npc has no AP With aiData.bAction = AI_ACTION_NONE and aiData.bNextAction = AI_ACTION_END_TURN, the npc turn is correctly ended in the next call to TurnBasedHandleNPCAI() --- TacticalAI/DecideAction.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index d8205b121..1a0de48c8 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -2493,6 +2493,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if ( pSoldier->bActionPoints <= 0 ) //Action points can be negative { pSoldier->aiData.usActionData = NOWHERE; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; return(AI_ACTION_NONE); } @@ -5118,6 +5119,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) if (pSoldier->bActionPoints <= 0) { pSoldier->aiData.usActionData = NOWHERE; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; return(AI_ACTION_NONE); } From 0ed68b6214617438b2d43382fca75df0a2cb4800 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 4 Feb 2024 16:17:36 +0200 Subject: [PATCH 31/80] Set functions to static --- TacticalAI/DecideAction.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 1a0de48c8..cf9ba2fa0 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -73,14 +73,14 @@ STR8 gStr8Team[] = { "OUR_TEAM", "ENEMY_TEAM", "CREATURE_TEAM", "MILITIA_TEAM", STR8 gStr8Class[] = { "SOLDIER_CLASS_NONE", "SOLDIER_CLASS_ADMINISTRATOR", "SOLDIER_CLASS_ELITE", "SOLDIER_CLASS_ARMY", "SOLDIER_CLASS_GREEN_MILITIA", "SOLDIER_CLASS_REG_MILITIA", "SOLDIER_CLASS_ELITE_MILITIA", "SOLDIER_CLASS_CREATURE", "SOLDIER_CLASS_MINER", "SOLDIER_CLASS_ZOMBIE", "SOLDIER_CLASS_TANK", "SOLDIER_CLASS_JEEP", "SOLDIER_CLASS_BANDIT", "SOLDIER_CLASS_ROBOT" }; STR8 gStr8Knowledge[] = { "HEARD_3_TURNS_AGO", "HEARD_2_TURNS_AGO", "HEARD_LAST_TURN", "HEARD_THIS_TURN", "NOT_HEARD_OR_SEEN", "SEEN_CURRENTLY", "SEEN_THIS_TURN", "SEEN_LAST_TURN", "SEEN_2_TURNS_AGO", "SEEN_3_TURNS_AGO" }; -void DoneScheduleAction( SOLDIERTYPE * pSoldier ) +static void DoneScheduleAction( SOLDIERTYPE * pSoldier ) { pSoldier->aiData.fAIFlags &= (~AI_CHECK_SCHEDULE); pSoldier->bAIScheduleProgress = 0; PostNextSchedule( pSoldier ); } -INT8 DecideActionSchedule( SOLDIERTYPE * pSoldier ) +static INT8 DecideActionSchedule( SOLDIERTYPE * pSoldier ) { SCHEDULENODE * pSchedule; INT32 iScheduleIndex; From 4faccb2957ce82a1b2e66f9f70c0c0f208b57cdd Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 4 Feb 2024 20:17:09 +0200 Subject: [PATCH 32/80] Fix indentation --- TacticalAI/Movement.cpp | 200 ++++++++++++++++++++-------------------- 1 file changed, 98 insertions(+), 102 deletions(-) diff --git a/TacticalAI/Movement.cpp b/TacticalAI/Movement.cpp index ec5daaaf2..3d9c7e1cc 100644 --- a/TacticalAI/Movement.cpp +++ b/TacticalAI/Movement.cpp @@ -432,7 +432,7 @@ INT8 RandomPointPatrolAI(SOLDIERTYPE *pSoldier) INT32 InternalGoAsFarAsPossibleTowards(SOLDIERTYPE *pSoldier, INT32 sDesGrid, INT16 bReserveAPs, INT8 bAction, INT8 fFlags ) { #ifdef DEBUGDECISIONS - STR16 tempstr; + STR16 tempstr; #endif INT32 sLoop,sAPCost; INT32 sTempDest,sGoToGrid; @@ -583,156 +583,152 @@ INT32 InternalGoAsFarAsPossibleTowards(SOLDIERTYPE *pSoldier, INT32 sDesGrid, IN } } - // HAVE FOUND AN OK destination AND PLOTTED A VALID BEST PATH TO IT + // HAVE FOUND AN OK destination AND PLOTTED A VALID BEST PATH TO IT #ifdef DEBUGDECISIONS - AINumMessage("Chosen legal destination is gridno ",sDesGrid); - AINumMessage("Tracing along path, pathRouteToGo = ",pSoldier->pathing.usPathIndex); + AINumMessage("Chosen legal destination is gridno ",sDesGrid); + AINumMessage("Tracing along path, pathRouteToGo = ",pSoldier->pathing.usPathIndex); #endif - sGoToGrid = pSoldier->sGridNo; // start back where soldier is standing now - sAPCost = 0; // initialize path cost counter + sGoToGrid = pSoldier->sGridNo; // start back where soldier is standing now + sAPCost = 0; // initialize path cost counter - // 0verhaul: If the destination is within the patrol route, allow it. This is necessary to allow an errant soldier - // to return to his patrol route if flanking or other actions have pulled him beyond his allowed range from origin. - if (SpacesAway(sOrigin,sDesGrid) <= usMaxDist) - { - fAllowDest = TRUE; - } + // 0verhaul: If the destination is within the patrol route, allow it. This is necessary to allow an errant soldier + // to return to his patrol route if flanking or other actions have pulled him beyond his allowed range from origin. + if (SpacesAway(sOrigin,sDesGrid) <= usMaxDist) + { + fAllowDest = TRUE; + } - // we'll only go as far along the plotted route as is within our - // permitted roaming range, and we'll stop as soon as we're down to <= 5 APs + // we'll only go as far along the plotted route as is within our + // permitted roaming range, and we'll stop as soon as we're down to <= 5 APs sTempDest = pSoldier->sGridNo; - for (sLoop = 0; sLoop < (pSoldier->pathing.usPathDataSize - pSoldier->pathing.usPathIndex); sLoop++) + for (sLoop = 0; sLoop < (pSoldier->pathing.usPathDataSize - pSoldier->pathing.usPathIndex); sLoop++) { - // what is the next gridno in the path? + // what is the next gridno in the path? - //sTempDest = NewGridNo( sGoToGrid,DirectionInc( (INT16) (pSoldier->pathing.usPathingData[sLoop] + 1) ) ); - //sTempDest = NewGridNo( sGoToGrid,DirectionInc( (UINT8) (pSoldier->pathing.usPathingData[sLoop]) ) ); - sTempDest = NewGridNo( sTempDest,DirectionInc( (UINT8) (pSoldier->pathing.usPathingData[sLoop]) ) ); + //sTempDest = NewGridNo( sGoToGrid,DirectionInc( (INT16) (pSoldier->pathing.usPathingData[sLoop] + 1) ) ); + //sTempDest = NewGridNo( sGoToGrid,DirectionInc( (UINT8) (pSoldier->pathing.usPathingData[sLoop]) ) ); + sTempDest = NewGridNo( sTempDest,DirectionInc( (UINT8) (pSoldier->pathing.usPathingData[sLoop]) ) ); - // this should NEVER be out of bounds - if (sTempDest == sGoToGrid) - { + // this should NEVER be out of bounds + if (sTempDest == sGoToGrid) + { #ifdef BETAVERSION - sprintf(tempstr,"GoAsFarAsPossibleTowards: ERROR - gridno along valid route is invalid! guynum %d, sTempDest = %d",pSoldier->ubID,sTempDest); + sprintf(tempstr,"GoAsFarAsPossibleTowards: ERROR - gridno along valid route is invalid! guynum %d, sTempDest = %d",pSoldier->ubID,sTempDest); #ifdef RECORDNET - fprintf(NetDebugFile,"\n\t%s\n",tempstr); + fprintf(NetDebugFile,"\n\t%s\n",tempstr); #endif - PopMessage(tempstr); - SaveGame(ERROR_SAVE); + PopMessage(tempstr); + SaveGame(ERROR_SAVE); #endif + break; // quit here, sGoToGrid is where we are going + } - break; // quit here, sGoToGrid is where we are going - } - - // if this takes us beyond our permitted "roaming range" - // but if it brings us closer, then allow it! - if (SpacesAway(sOrigin,sTempDest) > usMaxDist && !fAllowDest) - break; // quit here, sGoToGrid is where we are going + // if this takes us beyond our permitted "roaming range" + // but if it brings us closer, then allow it! + if (SpacesAway(sOrigin,sTempDest) > usMaxDist && !fAllowDest) + break; // quit here, sGoToGrid is where we are going - if ( usRoomRequired ) - { - if ( !( InARoom( sTempDest, &usTempRoom ) && usTempRoom == usRoomRequired ) ) + if ( usRoomRequired ) { - // quit here, limited by room! - break; + if ( !( InARoom( sTempDest, &usTempRoom ) && usTempRoom == usRoomRequired ) ) + { + // quit here, limited by room! + break; + } } - } - if ( (fFlags & FLAG_STOPSHORT) && SpacesAway( sDesGrid, sTempDest ) <= STOPSHORTDIST ) - { - break; // quit here, sGoToGrid is where we are going - } + if ( (fFlags & FLAG_STOPSHORT) && SpacesAway( sDesGrid, sTempDest ) <= STOPSHORTDIST ) + { + break; // quit here, sGoToGrid is where we are going + } - // CAN'T CALL PathCost() HERE! IT CALLS findBestPath() and overwrites - // pathRouteToGo !!! Gotta calculate the cost ourselves - Ian - // - //ubAPsLeft = pSoldier->bActionPoints - PathCost(pSoldier,sTempDest,FALSE,FALSE,FALSE,FALSE,FALSE); + // CAN'T CALL PathCost() HERE! IT CALLS findBestPath() and overwrites + // pathRouteToGo !!! Gotta calculate the cost ourselves - Ian + // + //ubAPsLeft = pSoldier->bActionPoints - PathCost(pSoldier,sTempDest,FALSE,FALSE,FALSE,FALSE,FALSE); - if (gfTurnBasedAI) - { - // if we're just starting the "costing" process (first gridno) - if (sLoop == 0) - { - if (pSoldier->usUIMovementMode == RUNNING) + if (gfTurnBasedAI) + { + // if we're just starting the "costing" process (first gridno) + if (sLoop == 0) { - sAPCost += GetAPsStartRun( pSoldier ); // changed by SANDRO - } + if (pSoldier->usUIMovementMode == RUNNING) + { + sAPCost += GetAPsStartRun( pSoldier ); // changed by SANDRO + } } - // ATE: Direction here? - sAPCost += EstimateActionPointCost( pSoldier, sTempDest, (INT8) pSoldier->pathing.usPathingData[sLoop], pSoldier->usUIMovementMode, (INT8) sLoop, (INT8) pSoldier->pathing.usPathDataSize ); - - bAPsLeft = pSoldier->bActionPoints - sAPCost; - } + // ATE: Direction here? + sAPCost += EstimateActionPointCost( pSoldier, sTempDest, (INT8) pSoldier->pathing.usPathingData[sLoop], pSoldier->usUIMovementMode, (INT8) sLoop, (INT8) pSoldier->pathing.usPathDataSize ); + bAPsLeft = pSoldier->bActionPoints - sAPCost; + } - // if this gridno is NOT a legal NPC destination - // DONT'T test path again - that would replace the traced path! - Ian - // NOTE: It's OK to go *THROUGH* water to try and get to the destination! - // sevenfm: jump over fence code - if current gridno is not legal destination, check next gridno - if (!LegalNPCDestination(pSoldier,sTempDest,IGNORE_PATH,WATEROK,0)) - { - // break; - continue; - } + // if this gridno is NOT a legal NPC destination + // DONT'T test path again - that would replace the traced path! - Ian + // NOTE: It's OK to go *THROUGH* water to try and get to the destination! + // sevenfm: jump over fence code - if current gridno is not legal destination, check next gridno + if (!LegalNPCDestination(pSoldier,sTempDest,IGNORE_PATH,WATEROK,0)) + { + // break; + continue; + } - // if after this, we have <= 5 APs remaining, that's far enough, break out - // (the idea is to preserve APs so we can crouch or react if - // necessary, and benefit from the carry-over next turn if not needed) - // This routine is NOT used by any GREEN AI, so such caution is warranted! + // if after this, we have <= 5 APs remaining, that's far enough, break out + // (the idea is to preserve APs so we can crouch or react if + // necessary, and benefit from the carry-over next turn if not needed) + // This routine is NOT used by any GREEN AI, so such caution is warranted! - if ( gfTurnBasedAI && (bAPsLeft < bReserveAPs) ) - break; - else + if ( gfTurnBasedAI && (bAPsLeft < bReserveAPs) ) + break; + else { - sGoToGrid = sTempDest; // we're OK up to here + sGoToGrid = sTempDest; // we're OK up to here - // if exactly 5 APs left, don't bother checking any further - if ( gfTurnBasedAI && (bAPsLeft == bReserveAPs) ) - break; + // if exactly 5 APs left, don't bother checking any further + if ( gfTurnBasedAI && (bAPsLeft == bReserveAPs) ) + break; } } - // if it turned out we couldn't go even 1 tile towards the desired gridno - if (sGoToGrid == pSoldier->sGridNo) + // if it turned out we couldn't go even 1 tile towards the desired gridno + if (sGoToGrid == pSoldier->sGridNo) { #ifdef DEBUGDECISIONS - sprintf(tempstr,"%s will go NOWHERE, path doesn't meet criteria",pSoldier->name); - AIPopMessage(tempstr); + sprintf(tempstr,"%s will go NOWHERE, path doesn't meet criteria",pSoldier->name); + AIPopMessage(tempstr); #endif - pSoldier->pathing.usPathIndex = pSoldier->pathing.usPathDataSize = 0; - return(NOWHERE); // then go nowhere - } - else - { - // possible optimization - stored path IS good if we're going all the way - if (sGoToGrid == sDesGrid) - { - pSoldier->pathing.bPathStored = TRUE; - pSoldier->pathing.sFinalDestination = sGoToGrid; + pSoldier->pathing.usPathIndex = pSoldier->pathing.usPathDataSize = 0; + return(NOWHERE); // then go nowhere } - else if ( pSoldier->pathing.usPathIndex == 0 ) + else { - // we can hack this surely! -- CJC - pSoldier->pathing.bPathStored = TRUE; - pSoldier->pathing.sFinalDestination = sGoToGrid; - pSoldier->pathing.usPathDataSize = sLoop + 1; - } + // possible optimization - stored path IS good if we're going all the way + if (sGoToGrid == sDesGrid) + { + pSoldier->pathing.bPathStored = TRUE; + pSoldier->pathing.sFinalDestination = sGoToGrid; + } + else if ( pSoldier->pathing.usPathIndex == 0 ) + { + // we can hack this surely! -- CJC + pSoldier->pathing.bPathStored = TRUE; + pSoldier->pathing.sFinalDestination = sGoToGrid; + pSoldier->pathing.usPathDataSize = sLoop + 1; + } #ifdef DEBUGDECISIONS ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"%d to %d with %d APs left", pSoldier->ubID, sGoToGrid, pSoldier->bActionPoints ); #endif - - return( sGoToGrid ); } } From 79372d5d23bbd54a0275889b2a4d0f43cfaae18b Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 4 Feb 2024 21:05:54 +0200 Subject: [PATCH 33/80] Try to find a cover spot when advancing to attack Calling InternalGoAsFarAsPossibleTowards() with target's gridno means AI doesn't take into account eg. nearby friendlies resulting in them bunching up and making a beeline straight at whoever they're trying to attack. We'll first at least attempt to find a cover spot near said target, if it doesn't work, only then head straight at them. --- TacticalAI/DecideAction.cpp | 13 +++++++++- TacticalAI/FindLocations.cpp | 46 ++++++++++++++++++++---------------- TacticalAI/ai.h | 2 +- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index cf9ba2fa0..6c225e62b 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -6697,7 +6697,18 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) INT8 oldOrders = pSoldier->aiData.bOrders; pSoldier->aiData.sPatrolGrid[0] = pSoldier->sGridNo; pSoldier->aiData.bOrders = CLOSEPATROL; - pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards( pSoldier, sClosestOpponent, BestAttack.ubAPCost, AI_ACTION_GET_CLOSER, 0 ); + // Try to find a cover spot near opponent + iCoverPercentBetter = 0; + INT32 spotNearTarget = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iCoverPercentBetter, sClosestOpponent); + if (spotNearTarget != NOWHERE) + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, spotNearTarget, BestAttack.ubAPCost, AI_ACTION_GET_CLOSER, 0); + + } + else + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards( pSoldier, sClosestOpponent, BestAttack.ubAPCost, AI_ACTION_GET_CLOSER, 0 ); + } pSoldier->aiData.sPatrolGrid[0] = tgrd; pSoldier->aiData.bOrders = oldOrders; diff --git a/TacticalAI/FindLocations.cpp b/TacticalAI/FindLocations.cpp index 37fcd9846..b57ca0cc8 100644 --- a/TacticalAI/FindLocations.cpp +++ b/TacticalAI/FindLocations.cpp @@ -613,6 +613,7 @@ UINT8 NumberOfTeamMatesAdjacent( SOLDIERTYPE * pSoldier, INT32 sGridNo ) } INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentBetter) +INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentBetter, INT32 targetGridNo) { DebugMsg(TOPIC_JA2AI,DBG_LEVEL_3,String("FindBestNearbyCover")); @@ -649,6 +650,11 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB BOOLEAN fProneCover; UINT8 ubDiff = SoldierDifficultyLevel( pSoldier ); + if (targetGridNo == NOWHERE) + { + targetGridNo = pSoldier->sGridNo; + } + // There's no cover when boxing! if (gTacticalStatus.bBoxingState == BOXING) { @@ -710,9 +716,9 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB }*/ // maximum search range is 1 tile / 8 pts of wisdom - if (iSearchRange > (pSoldier->stats.bWisdom / 8)) + if (iSearchRange > (pSoldier->stats.bWisdom / 4)) { - iSearchRange = (pSoldier->stats.bWisdom / 8); + iSearchRange = (pSoldier->stats.bWisdom / 4); } if (!gfTurnBasedAI) @@ -777,13 +783,13 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB if (Threat[uiLoop].iOrigRange <= MAX_THREAT_RANGE) { // 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); + iCurrentCoverValue += CalcCoverValue(pSoldier,targetGridNo,iMyThreatValue,pSoldier->bActionPoints,uiLoop,Threat[uiLoop].iOrigRange,morale,&iCurrentScale); } // sevenfm: sight test if( gGameExternalOptions.fAIBetterCover ) { - if ( LocationToLocationLineOfSightTest( Threat[uiLoop].sGridNo, Threat[uiLoop].pOpponent->pathing.bLevel, pSoldier->sGridNo, pSoldier->pathing.bLevel, TRUE, MAX_VISION_RANGE, STANDING_LOS_POS, PRONE_LOS_POS ) ) - //if ( SoldierToVirtualSoldierLineOfSightTest( Threat[uiLoop].pOpponent, pSoldier->sGridNo, pSoldier->pathing.bLevel, ANIM_PRONE, TRUE, NO_DISTANCE_LIMIT ) ) + if ( LocationToLocationLineOfSightTest( Threat[uiLoop].sGridNo, Threat[uiLoop].pOpponent->pathing.bLevel, targetGridNo, pSoldier->pathing.bLevel, TRUE, MAX_VISION_RANGE, STANDING_LOS_POS, PRONE_LOS_POS ) ) + //if ( SoldierToVirtualSoldierLineOfSightTest( Threat[uiLoop].pOpponent, targetGridNo, pSoldier->pathing.bLevel, ANIM_PRONE, TRUE, NO_DISTANCE_LIMIT ) ) { fProneCover = FALSE; } @@ -796,12 +802,12 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // by 10% (so locations next to several people will be very much frowned upon if ( iCurrentCoverValue >= 0 ) { - iCurrentCoverValue -= (iCurrentCoverValue / 10) * NumberOfTeamMatesAdjacent( pSoldier, pSoldier->sGridNo ); + iCurrentCoverValue -= (iCurrentCoverValue / 5) * NumberOfTeamMatesAdjacent( pSoldier, targetGridNo ); } else { // when negative, must add a negative to decrease the total - iCurrentCoverValue += (iCurrentCoverValue / 10) * NumberOfTeamMatesAdjacent( pSoldier, pSoldier->sGridNo ); + iCurrentCoverValue += (iCurrentCoverValue / 5) * NumberOfTeamMatesAdjacent( pSoldier, targetGridNo ); } if( gGameExternalOptions.fAIBetterCover ) @@ -816,17 +822,17 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB } // sevenfm: check for nearby friends, add bonus/penalty - ubNearbyFriends = __min(5, CountNearbyFriends( pSoldier, pSoldier->sGridNo, 5 )); + ubNearbyFriends = __min(5, CountNearbyFriends( pSoldier, targetGridNo, 5 )); iCurrentCoverValue -= ubNearbyFriends * abs(iCurrentCoverValue) / (10-ubDiff); // sevenfm: penalize locations with fresh corpses - if(GetNearestRottingCorpseAIWarning( pSoldier->sGridNo ) > 0) + if(GetNearestRottingCorpseAIWarning( targetGridNo ) > 0) { iCurrentCoverValue -= abs(iCurrentCoverValue) / (8-ubDiff); } // sevenfm: penalize locations near red smoke - iCurrentCoverValue -= abs(iCurrentCoverValue) * RedSmokeDanger(pSoldier->sGridNo, pSoldier->pathing.bLevel) / 100; + iCurrentCoverValue -= abs(iCurrentCoverValue) * RedSmokeDanger(targetGridNo, pSoldier->pathing.bLevel) / 100; } #ifdef DEBUGCOVER @@ -834,15 +840,15 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB #endif // determine maximum horizontal limits - sMaxLeft = min(iSearchRange,(pSoldier->sGridNo % MAXCOL)); + sMaxLeft = min(iSearchRange,(targetGridNo % MAXCOL)); //NumMessage("sMaxLeft = ",sMaxLeft); - sMaxRight = min(iSearchRange,MAXCOL - ((pSoldier->sGridNo % MAXCOL) + 1)); + sMaxRight = min(iSearchRange,MAXCOL - ((targetGridNo % MAXCOL) + 1)); //NumMessage("sMaxRight = ",sMaxRight); // determine maximum vertical limits - sMaxUp = min(iSearchRange,(pSoldier->sGridNo / MAXROW)); + sMaxUp = min(iSearchRange,(targetGridNo / MAXROW)); //NumMessage("sMaxUp = ",sMaxUp); - sMaxDown = min(iSearchRange,MAXROW - ((pSoldier->sGridNo / MAXROW) + 1)); + sMaxDown = min(iSearchRange,MAXROW - ((targetGridNo / MAXROW) + 1)); //NumMessage("sMaxDown = ",sMaxDown); iRoamRange = RoamingRange(pSoldier,&sOrigin); @@ -852,7 +858,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB (!TileIsOutOfBounds(sOrigin))) { // must try to stay within or return to the point of origin - iDistFromOrigin = SpacesAway(sOrigin,pSoldier->sGridNo); + iDistFromOrigin = SpacesAway(sOrigin,targetGridNo); } else { @@ -899,7 +905,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB { for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++) { - sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset); + sGridNo = targetGridNo + sXOffset + (MAXCOL * sYOffset); if ( !(sGridNo >=0 && sGridNo < WORLD_MAX) ) { continue; @@ -912,7 +918,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // Turn off the "reachable" flag for his current location // so we don't consider it - gpWorldLevelData[pSoldier->sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE); + gpWorldLevelData[targetGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE); // SET UP DOUBLE-LOOP TO STEP THROUGH POTENTIAL GRID #s for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++) @@ -922,7 +928,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB //HandleMyMouseCursor(KEYBOARDALSO); // calculate the next potential gridno - sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset); + sGridNo = targetGridNo + sXOffset + (MAXCOL * sYOffset); if ( !(sGridNo >=0 && sGridNo < WORLD_MAX) ) { continue; @@ -979,7 +985,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // sevenfm: avoid moving into light if (InLightAtNight(sGridNo, pSoldier->pathing.bLevel) && - !InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + !InLightAtNight(targetGridNo, pSoldier->pathing.bLevel) && !pSoldier->aiData.bUnderFire) { continue; @@ -988,7 +994,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // avoid moving into red smoke if (gGameExternalOptions.fAIBetterCover && RedSmokeDanger(sGridNo, pSoldier->pathing.bLevel) && - !RedSmokeDanger(pSoldier->sGridNo, pSoldier->pathing.bLevel)) + !RedSmokeDanger(targetGridNo, pSoldier->pathing.bLevel)) { //DebugCover(pSoldier, String("moving into red smoke, skip")); continue; diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index cd25caf57..60f0f45c9 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -199,7 +199,7 @@ void EndAIGuysTurn( SOLDIERTYPE *pSoldier ); INT8 ExecuteAction(SOLDIERTYPE *pSoldier); INT32 FindAdjacentSpotBeside(SOLDIERTYPE *pSoldier, INT32 sGridNo); -INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *pPercentBetter); +INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *pPercentBetter, INT32 targetGridNo = NOWHERE); INT32 FindClosestDoor( SOLDIERTYPE * pSoldier ); INT32 FindNearbyPointOnEdgeOfMap( SOLDIERTYPE * pSoldier, INT8 * pbDirection ); INT32 FindNearestEdgePoint( INT32 sGridNo ); From 5c86cf0b07cb84627774511c323882b2bb054520 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 10 Feb 2024 18:12:30 +0200 Subject: [PATCH 34/80] Attempt to continue moving towards enemy when in water. If our current or last action was AI_ACTION_SEEK_OPPONENT. The AI tends to flipflop between jumping into water, swimming a certain distance and then "panic" because they're in the water and then try to reach the closest land tile. Which is often right where they entered the water. Adding a bit of decision momentum with this change should help with that. --- TacticalAI/DecideAction.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 6c225e62b..2fb4b0f55 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -10610,7 +10610,7 @@ ActionType DecideActionStuckInWaterOrGas(SOLDIERTYPE *pSoldier, BOOLEAN ubCanMov DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide action if stuck in water or gas]")); // when in deep water, move to closest opponent - if (ubCanMove && (bInDeepWater || bInWater) && !pSoldier->aiData.bNeutral && pSoldier->aiData.bOrders == SEEKENEMY) + if (ubCanMove && (bInDeepWater || bInWater) && !pSoldier->aiData.bNeutral && (pSoldier->aiData.bOrders == SEEKENEMY || pSoldier->aiData.bAction == AI_ACTION_SEEK_OPPONENT || pSoldier->aiData.bLastAction == AI_ACTION_SEEK_OPPONENT)) { // find closest reachable opponent, excluding opponents in deep water BOOLEAN fClimbDummy; From 0240729c112a405aa7d92f3c6586a206ca8af7dc Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 10 Feb 2024 18:13:20 +0200 Subject: [PATCH 35/80] Use correct string array --- TacticalAI/AIMain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index ff85945f3..654809707 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -1016,7 +1016,7 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named if (!ShouldActionStayInProgress(pSoldier)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Action % s was stuck as being in progress. Canceling action", wszAction[pSoldier->aiData.bAction])); + DebugAI(AI_MSG_INFO, pSoldier, String("Action %s was stuck as being in progress. Canceling action", szAction[pSoldier->aiData.bAction])); ActionDone(pSoldier); } } From 1aec54f59c54812e912d2e7ffe801425be28725d Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 10 Feb 2024 18:14:35 +0200 Subject: [PATCH 36/80] add another AI logging entry --- TacticalAI/AIMain.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 654809707..73d768a27 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -1739,6 +1739,8 @@ void TurnBasedHandleNPCAI(SOLDIERTYPE *pSoldier) #ifdef DEBUGBUSY AINumMessage("Busy with action, skipping guy#",pSoldier->ubID); #endif + ScreenMsg(FONT_MCOLOR_LTRED, MSG_INTERFACE, L"Busy with action %s, skipping guy [%d]", wszAction[pSoldier->aiData.bAction], pSoldier->ubID); + DebugAI(AI_MSG_INFO, pSoldier, String("Busy with action %s, skipping guy [%d]", wszAction[pSoldier->aiData.bAction], pSoldier->ubID)); // let it continue return; From 3c2b0483673acf8460f36c38b802d4bfc5ddfbca Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 10 Feb 2024 18:12:30 +0200 Subject: [PATCH 37/80] Attempt to continue moving towards enemy when in water. If our current or last action was AI_ACTION_SEEK_OPPONENT. The AI tends to flipflop between jumping into water, swimming a certain distance and then "panic" because they're in the water and then try to reach the closest land tile. Which is often right where they entered the water. Adding a bit of decision momentum with this change should help with that. --- TacticalAI/DecideAction.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index cf9ba2fa0..680b9c309 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -10599,7 +10599,7 @@ ActionType DecideActionStuckInWaterOrGas(SOLDIERTYPE *pSoldier, BOOLEAN ubCanMov DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide action if stuck in water or gas]")); // when in deep water, move to closest opponent - if (ubCanMove && (bInDeepWater || bInWater) && !pSoldier->aiData.bNeutral && pSoldier->aiData.bOrders == SEEKENEMY) + if (ubCanMove && (bInDeepWater || bInWater) && !pSoldier->aiData.bNeutral && (pSoldier->aiData.bOrders == SEEKENEMY || pSoldier->aiData.bAction == AI_ACTION_SEEK_OPPONENT || pSoldier->aiData.bLastAction == AI_ACTION_SEEK_OPPONENT)) { // find closest reachable opponent, excluding opponents in deep water BOOLEAN fClimbDummy; From eaf76957c0566c34ab29fda362989add88e65664 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 10 Feb 2024 18:13:20 +0200 Subject: [PATCH 38/80] Use correct string array --- TacticalAI/AIMain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index ff85945f3..654809707 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -1016,7 +1016,7 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named if (!ShouldActionStayInProgress(pSoldier)) { - DebugAI(AI_MSG_INFO, pSoldier, String("Action % s was stuck as being in progress. Canceling action", wszAction[pSoldier->aiData.bAction])); + DebugAI(AI_MSG_INFO, pSoldier, String("Action %s was stuck as being in progress. Canceling action", szAction[pSoldier->aiData.bAction])); ActionDone(pSoldier); } } From f6052f0eff9e6235d47103da15858dee2f91d740 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 10 Feb 2024 18:14:35 +0200 Subject: [PATCH 39/80] add another AI logging entry --- TacticalAI/AIMain.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 654809707..73d768a27 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -1739,6 +1739,8 @@ void TurnBasedHandleNPCAI(SOLDIERTYPE *pSoldier) #ifdef DEBUGBUSY AINumMessage("Busy with action, skipping guy#",pSoldier->ubID); #endif + ScreenMsg(FONT_MCOLOR_LTRED, MSG_INTERFACE, L"Busy with action %s, skipping guy [%d]", wszAction[pSoldier->aiData.bAction], pSoldier->ubID); + DebugAI(AI_MSG_INFO, pSoldier, String("Busy with action %s, skipping guy [%d]", wszAction[pSoldier->aiData.bAction], pSoldier->ubID)); // let it continue return; From 93acd19c111b263a8cd8f0cd0997dcefa2ace52f Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 4 Feb 2024 21:05:54 +0200 Subject: [PATCH 40/80] Try to find a cover spot when advancing to attack Calling InternalGoAsFarAsPossibleTowards() with target's gridno means AI doesn't take into account eg. nearby friendlies resulting in them bunching up and making a beeline straight at whoever they're trying to attack. We'll first at least attempt to find a cover spot near said target, if it doesn't work, only then head straight at them. --- TacticalAI/DecideAction.cpp | 13 +++++++++- TacticalAI/FindLocations.cpp | 47 ++++++++++++++++++++---------------- TacticalAI/ai.h | 2 +- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 680b9c309..2fb4b0f55 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -6697,7 +6697,18 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) INT8 oldOrders = pSoldier->aiData.bOrders; pSoldier->aiData.sPatrolGrid[0] = pSoldier->sGridNo; pSoldier->aiData.bOrders = CLOSEPATROL; - pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards( pSoldier, sClosestOpponent, BestAttack.ubAPCost, AI_ACTION_GET_CLOSER, 0 ); + // Try to find a cover spot near opponent + iCoverPercentBetter = 0; + INT32 spotNearTarget = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iCoverPercentBetter, sClosestOpponent); + if (spotNearTarget != NOWHERE) + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, spotNearTarget, BestAttack.ubAPCost, AI_ACTION_GET_CLOSER, 0); + + } + else + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards( pSoldier, sClosestOpponent, BestAttack.ubAPCost, AI_ACTION_GET_CLOSER, 0 ); + } pSoldier->aiData.sPatrolGrid[0] = tgrd; pSoldier->aiData.bOrders = oldOrders; diff --git a/TacticalAI/FindLocations.cpp b/TacticalAI/FindLocations.cpp index 37fcd9846..20f72e6cb 100644 --- a/TacticalAI/FindLocations.cpp +++ b/TacticalAI/FindLocations.cpp @@ -612,7 +612,7 @@ UINT8 NumberOfTeamMatesAdjacent( SOLDIERTYPE * pSoldier, INT32 sGridNo ) return( ubCount ); } -INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentBetter) +INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentBetter, INT32 targetGridNo) { DebugMsg(TOPIC_JA2AI,DBG_LEVEL_3,String("FindBestNearbyCover")); @@ -649,6 +649,11 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB BOOLEAN fProneCover; UINT8 ubDiff = SoldierDifficultyLevel( pSoldier ); + if (targetGridNo == NOWHERE) + { + targetGridNo = pSoldier->sGridNo; + } + // There's no cover when boxing! if (gTacticalStatus.bBoxingState == BOXING) { @@ -710,9 +715,9 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB }*/ // maximum search range is 1 tile / 8 pts of wisdom - if (iSearchRange > (pSoldier->stats.bWisdom / 8)) + if (iSearchRange > (pSoldier->stats.bWisdom / 4)) { - iSearchRange = (pSoldier->stats.bWisdom / 8); + iSearchRange = (pSoldier->stats.bWisdom / 4); } if (!gfTurnBasedAI) @@ -777,13 +782,13 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB if (Threat[uiLoop].iOrigRange <= MAX_THREAT_RANGE) { // 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); + iCurrentCoverValue += CalcCoverValue(pSoldier,targetGridNo,iMyThreatValue,pSoldier->bActionPoints,uiLoop,Threat[uiLoop].iOrigRange,morale,&iCurrentScale); } // sevenfm: sight test if( gGameExternalOptions.fAIBetterCover ) { - if ( LocationToLocationLineOfSightTest( Threat[uiLoop].sGridNo, Threat[uiLoop].pOpponent->pathing.bLevel, pSoldier->sGridNo, pSoldier->pathing.bLevel, TRUE, MAX_VISION_RANGE, STANDING_LOS_POS, PRONE_LOS_POS ) ) - //if ( SoldierToVirtualSoldierLineOfSightTest( Threat[uiLoop].pOpponent, pSoldier->sGridNo, pSoldier->pathing.bLevel, ANIM_PRONE, TRUE, NO_DISTANCE_LIMIT ) ) + if ( LocationToLocationLineOfSightTest( Threat[uiLoop].sGridNo, Threat[uiLoop].pOpponent->pathing.bLevel, targetGridNo, pSoldier->pathing.bLevel, TRUE, MAX_VISION_RANGE, STANDING_LOS_POS, PRONE_LOS_POS ) ) + //if ( SoldierToVirtualSoldierLineOfSightTest( Threat[uiLoop].pOpponent, targetGridNo, pSoldier->pathing.bLevel, ANIM_PRONE, TRUE, NO_DISTANCE_LIMIT ) ) { fProneCover = FALSE; } @@ -796,12 +801,12 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // by 10% (so locations next to several people will be very much frowned upon if ( iCurrentCoverValue >= 0 ) { - iCurrentCoverValue -= (iCurrentCoverValue / 10) * NumberOfTeamMatesAdjacent( pSoldier, pSoldier->sGridNo ); + iCurrentCoverValue -= (iCurrentCoverValue / 5) * NumberOfTeamMatesAdjacent( pSoldier, targetGridNo ); } else { // when negative, must add a negative to decrease the total - iCurrentCoverValue += (iCurrentCoverValue / 10) * NumberOfTeamMatesAdjacent( pSoldier, pSoldier->sGridNo ); + iCurrentCoverValue += (iCurrentCoverValue / 5) * NumberOfTeamMatesAdjacent( pSoldier, targetGridNo ); } if( gGameExternalOptions.fAIBetterCover ) @@ -816,17 +821,17 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB } // sevenfm: check for nearby friends, add bonus/penalty - ubNearbyFriends = __min(5, CountNearbyFriends( pSoldier, pSoldier->sGridNo, 5 )); + ubNearbyFriends = __min(5, CountNearbyFriends( pSoldier, targetGridNo, 5 )); iCurrentCoverValue -= ubNearbyFriends * abs(iCurrentCoverValue) / (10-ubDiff); // sevenfm: penalize locations with fresh corpses - if(GetNearestRottingCorpseAIWarning( pSoldier->sGridNo ) > 0) + if(GetNearestRottingCorpseAIWarning( targetGridNo ) > 0) { iCurrentCoverValue -= abs(iCurrentCoverValue) / (8-ubDiff); } // sevenfm: penalize locations near red smoke - iCurrentCoverValue -= abs(iCurrentCoverValue) * RedSmokeDanger(pSoldier->sGridNo, pSoldier->pathing.bLevel) / 100; + iCurrentCoverValue -= abs(iCurrentCoverValue) * RedSmokeDanger(targetGridNo, pSoldier->pathing.bLevel) / 100; } #ifdef DEBUGCOVER @@ -834,15 +839,15 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB #endif // determine maximum horizontal limits - sMaxLeft = min(iSearchRange,(pSoldier->sGridNo % MAXCOL)); + sMaxLeft = min(iSearchRange,(targetGridNo % MAXCOL)); //NumMessage("sMaxLeft = ",sMaxLeft); - sMaxRight = min(iSearchRange,MAXCOL - ((pSoldier->sGridNo % MAXCOL) + 1)); + sMaxRight = min(iSearchRange,MAXCOL - ((targetGridNo % MAXCOL) + 1)); //NumMessage("sMaxRight = ",sMaxRight); // determine maximum vertical limits - sMaxUp = min(iSearchRange,(pSoldier->sGridNo / MAXROW)); + sMaxUp = min(iSearchRange,(targetGridNo / MAXROW)); //NumMessage("sMaxUp = ",sMaxUp); - sMaxDown = min(iSearchRange,MAXROW - ((pSoldier->sGridNo / MAXROW) + 1)); + sMaxDown = min(iSearchRange,MAXROW - ((targetGridNo / MAXROW) + 1)); //NumMessage("sMaxDown = ",sMaxDown); iRoamRange = RoamingRange(pSoldier,&sOrigin); @@ -852,7 +857,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB (!TileIsOutOfBounds(sOrigin))) { // must try to stay within or return to the point of origin - iDistFromOrigin = SpacesAway(sOrigin,pSoldier->sGridNo); + iDistFromOrigin = SpacesAway(sOrigin,targetGridNo); } else { @@ -899,7 +904,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB { for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++) { - sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset); + sGridNo = targetGridNo + sXOffset + (MAXCOL * sYOffset); if ( !(sGridNo >=0 && sGridNo < WORLD_MAX) ) { continue; @@ -912,7 +917,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // Turn off the "reachable" flag for his current location // so we don't consider it - gpWorldLevelData[pSoldier->sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE); + gpWorldLevelData[targetGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE); // SET UP DOUBLE-LOOP TO STEP THROUGH POTENTIAL GRID #s for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++) @@ -922,7 +927,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB //HandleMyMouseCursor(KEYBOARDALSO); // calculate the next potential gridno - sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset); + sGridNo = targetGridNo + sXOffset + (MAXCOL * sYOffset); if ( !(sGridNo >=0 && sGridNo < WORLD_MAX) ) { continue; @@ -979,7 +984,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // sevenfm: avoid moving into light if (InLightAtNight(sGridNo, pSoldier->pathing.bLevel) && - !InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + !InLightAtNight(targetGridNo, pSoldier->pathing.bLevel) && !pSoldier->aiData.bUnderFire) { continue; @@ -988,7 +993,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // avoid moving into red smoke if (gGameExternalOptions.fAIBetterCover && RedSmokeDanger(sGridNo, pSoldier->pathing.bLevel) && - !RedSmokeDanger(pSoldier->sGridNo, pSoldier->pathing.bLevel)) + !RedSmokeDanger(targetGridNo, pSoldier->pathing.bLevel)) { //DebugCover(pSoldier, String("moving into red smoke, skip")); continue; diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index cd25caf57..60f0f45c9 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -199,7 +199,7 @@ void EndAIGuysTurn( SOLDIERTYPE *pSoldier ); INT8 ExecuteAction(SOLDIERTYPE *pSoldier); INT32 FindAdjacentSpotBeside(SOLDIERTYPE *pSoldier, INT32 sGridNo); -INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *pPercentBetter); +INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *pPercentBetter, INT32 targetGridNo = NOWHERE); INT32 FindClosestDoor( SOLDIERTYPE * pSoldier ); INT32 FindNearbyPointOnEdgeOfMap( SOLDIERTYPE * pSoldier, INT8 * pbDirection ); INT32 FindNearestEdgePoint( INT32 sGridNo ); From b480a67d36e7b39568efabde1b77f389a13fae12 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 10 Feb 2024 18:47:03 +0200 Subject: [PATCH 41/80] Reduce code duplication --- TacticalAI/FindLocations.cpp | 232 ++++++++++++----------------------- 1 file changed, 81 insertions(+), 151 deletions(-) diff --git a/TacticalAI/FindLocations.cpp b/TacticalAI/FindLocations.cpp index b57ca0cc8..33faab039 100644 --- a/TacticalAI/FindLocations.cpp +++ b/TacticalAI/FindLocations.cpp @@ -612,7 +612,85 @@ UINT8 NumberOfTeamMatesAdjacent( SOLDIERTYPE * pSoldier, INT32 sGridNo ) return( ubCount ); } -INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentBetter) +static void CalculateCoverValue(SOLDIERTYPE* pSoldier, const INT32 sGridNo, const INT32 iPathCost, const INT32 iMyThreatValue, const UINT32 uiThreatCnt, const UINT8 ubDiff, const BOOLEAN fNight, const UINT8 ubBackgroundLightPercent, const INT32 morale, INT32 &iCoverValue, INT32& iCoverScale) +{ + iCoverValue = 0; + iCoverScale = 0; + + // sevenfm: check sight cover + BOOLEAN fProneCover = TRUE; + + // for every opponent that threatens, consider this spot's cover vs. him + for (UINT32 uiLoop = 0; uiLoop < uiThreatCnt; uiLoop++) + { + // calculate the range we would be at from this opponent + INT32 iThreatRange = Threat[uiLoop].iOrigRange; + if (sGridNo != pSoldier->sGridNo) + { + iThreatRange = GetRangeInCellCoordsFromGridNoDiff(sGridNo, Threat[uiLoop].sGridNo); + } + + // if this threat would be within 20 tiles, count it + if (iThreatRange <= MAX_THREAT_RANGE) + { + iCoverValue += CalcCoverValue( + pSoldier, sGridNo, iMyThreatValue, + (pSoldier->bActionPoints - iPathCost), + uiLoop, iThreatRange, morale, &iCoverScale + ); + } + + // sevenfm: sight test + if (gGameExternalOptions.fAIBetterCover) + { + if (LocationToLocationLineOfSightTest(Threat[uiLoop].sGridNo, Threat[uiLoop].pOpponent->pathing.bLevel, sGridNo, pSoldier->pathing.bLevel, TRUE, MAX_VISION_RANGE, STANDING_LOS_POS, PRONE_LOS_POS)) + //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); + } + + // reduce cover for each person adjacent to this gridno who is on our team, + // by 10% (so locations next to several people will be very much frowned upon + iCoverValue -= (abs(iCoverValue) / 5) * 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, add bonus/penalty 10% + UINT8 ubNearbyFriends = __max(0, CountNearbyFriends(pSoldier, sGridNo, 7)); + iCoverValue -= ubNearbyFriends * abs(iCoverValue) / (6 - ubDiff); + + // sevenfm: penalize locations with fresh corpses + if (GetNearestRottingCorpseAIWarning(sGridNo) > 0) + { + iCoverValue -= abs(iCoverValue) / (6 - ubDiff); + } + + // sevenfm: penalize locations near red smoke + iCoverValue -= abs(iCoverValue) * RedSmokeDanger(sGridNo, pSoldier->pathing.bLevel) / 100; + } + + 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 + // using the difference in sighting distance between the background and the + // light for this tile + //ubLightPercentDifference = (gbLightSighting[ 0 ][ LightTrueLevel( sGridNo, pSoldier->pathing.bLevel ) ] - ubBackgroundLightPercent ); + UINT8 ubLightPercentDifference = (gGameExternalOptions.ubBrightnessVisionMod[LightTrueLevel(sGridNo, pSoldier->pathing.bLevel)] - ubBackgroundLightPercent); + iCoverValue -= (abs(iCoverValue) / 100) * ubLightPercentDifference; + } +} + INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentBetter, INT32 targetGridNo) { DebugMsg(TOPIC_JA2AI,DBG_LEVEL_3,String("FindBestNearbyCover")); @@ -770,70 +848,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // calculate our current cover value in the place we are now, since the // cover we are searching for must be better than what we have now! - 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++) - { - // if this threat is CURRENTLY within 20 tiles - if (Threat[uiLoop].iOrigRange <= MAX_THREAT_RANGE) - { - // add this opponent's cover value to our current total cover value - iCurrentCoverValue += CalcCoverValue(pSoldier,targetGridNo,iMyThreatValue,pSoldier->bActionPoints,uiLoop,Threat[uiLoop].iOrigRange,morale,&iCurrentScale); - } - // sevenfm: sight test - if( gGameExternalOptions.fAIBetterCover ) - { - if ( LocationToLocationLineOfSightTest( Threat[uiLoop].sGridNo, Threat[uiLoop].pOpponent->pathing.bLevel, targetGridNo, pSoldier->pathing.bLevel, TRUE, MAX_VISION_RANGE, STANDING_LOS_POS, PRONE_LOS_POS ) ) - //if ( SoldierToVirtualSoldierLineOfSightTest( Threat[uiLoop].pOpponent, targetGridNo, pSoldier->pathing.bLevel, ANIM_PRONE, TRUE, NO_DISTANCE_LIMIT ) ) - { - fProneCover = FALSE; - } - } - //sprintf(tempstr,"iCurrentCoverValue after opponent %d is now %d",iLoop,iCurrentCoverValue); - //PopMessage(tempstr); - } - - // reduce cover for each person adjacent to this gridno who is on our team, - // by 10% (so locations next to several people will be very much frowned upon - if ( iCurrentCoverValue >= 0 ) - { - iCurrentCoverValue -= (iCurrentCoverValue / 5) * NumberOfTeamMatesAdjacent( pSoldier, targetGridNo ); - } - else - { - // when negative, must add a negative to decrease the total - iCurrentCoverValue += (iCurrentCoverValue / 5) * NumberOfTeamMatesAdjacent( pSoldier, targetGridNo ); - } - - 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, CountNearbyFriends( pSoldier, targetGridNo, 5 )); - iCurrentCoverValue -= ubNearbyFriends * abs(iCurrentCoverValue) / (10-ubDiff); - - // sevenfm: penalize locations with fresh corpses - if(GetNearestRottingCorpseAIWarning( targetGridNo ) > 0) - { - iCurrentCoverValue -= abs(iCurrentCoverValue) / (8-ubDiff); - } - - // sevenfm: penalize locations near red smoke - iCurrentCoverValue -= abs(iCurrentCoverValue) * RedSmokeDanger(targetGridNo, pSoldier->pathing.bLevel) / 100; - } + CalculateCoverValue(pSoldier, targetGridNo, 0, iMyThreatValue, uiThreatCnt, ubDiff, fNight, ubBackgroundLightPercent, morale, iCurrentCoverValue, iCurrentScale); #ifdef DEBUGCOVER // AINumMessage("Search Range = ",iSearchRange); @@ -1032,92 +1047,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // OK, this place shows potential. How useful is it as cover? // EVALUATE EACH GRID #, remembering the BEST PROTECTED ONE - 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++) - { - // calculate the range we would be at from this opponent - iThreatRange = GetRangeInCellCoordsFromGridNoDiff( sGridNo, Threat[uiLoop].sGridNo ); - // if this threat would be within 20 tiles, count it - if (iThreatRange <= MAX_THREAT_RANGE) - { - iCoverValue += CalcCoverValue(pSoldier,sGridNo,iMyThreatValue, - (pSoldier->bActionPoints - iPathCost), - uiLoop,iThreatRange,morale,&iCoverScale); - } - - // sevenfm: sight test - if( gGameExternalOptions.fAIBetterCover ) - { - if ( LocationToLocationLineOfSightTest( Threat[uiLoop].sGridNo, Threat[uiLoop].pOpponent->pathing.bLevel, sGridNo, pSoldier->pathing.bLevel, TRUE, MAX_VISION_RANGE, STANDING_LOS_POS, PRONE_LOS_POS ) ) - //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); - } - - // reduce cover for each person adjacent to this gridno who is on our team, - // by 10% (so locations next to several people will be very much frowned upon - if ( iCoverValue >= 0 ) - { - iCoverValue -= (iCoverValue / 10) * NumberOfTeamMatesAdjacent( pSoldier, sGridNo ); - } - else - { - // when negative, must add a negative to decrease the total - 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, CountNearbyFriends( pSoldier, sGridNo, 5 )); - iCoverValue -= ubNearbyFriends * abs(iCoverValue) / (10-ubDiff); - - // sevenfm: penalize locations with fresh corpses - if(GetNearestRottingCorpseAIWarning( sGridNo ) > 0) - { - iCoverValue -= abs(iCoverValue) / (8-ubDiff); - } - - // sevenfm: penalize locations near red smoke - iCoverValue -= abs(iCoverValue) * RedSmokeDanger(sGridNo, pSoldier->pathing.bLevel) / 100; - } - - 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 - // using the difference in sighting distance between the background and the - // light for this tile - //ubLightPercentDifference = (gbLightSighting[ 0 ][ LightTrueLevel( sGridNo, pSoldier->pathing.bLevel ) ] - ubBackgroundLightPercent ); - ubLightPercentDifference = (gGameExternalOptions.ubBrightnessVisionMod[ LightTrueLevel( sGridNo, pSoldier->pathing.bLevel ) ] - ubBackgroundLightPercent ); - - if ( iCoverValue >= 0 ) - { - iCoverValue -= (iCoverValue / 100) * ubLightPercentDifference; - } - else - { - iCoverValue += (iCoverValue / 100) * ubLightPercentDifference; - } - } - + CalculateCoverValue(pSoldier, sGridNo, iPathCost, iMyThreatValue, uiThreatCnt, ubDiff, fNight, ubBackgroundLightPercent, morale, iCoverValue, iCoverScale); #ifdef DEBUGCOVER // if there ARE multiple opponents From 877ec385ee61a94b34b67b7a73669e7f5a3a66db Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 10 Feb 2024 18:50:30 +0200 Subject: [PATCH 42/80] Whitespace changes --- TacticalAI/DecideAction.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 2fb4b0f55..bb3fbd013 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -2696,7 +2696,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } - //////////////////////////////////////////////////////////////////////// // IF POSSIBLE, FIRE LONG RANGE WEAPONS AT TARGETS REPORTED BY RADIO //////////////////////////////////////////////////////////////////////// @@ -2869,6 +2868,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) DebugAI(AI_MSG_TOPIC, pSoldier, String("[use smoke to cover friend]"), gLogDecideActionRed); CheckTossFriendSmoke(pSoldier, &BestThrow); + if (BestThrow.ubPossible) { DebugAI(AI_MSG_INFO, pSoldier, String("Throw possible"), gLogDecideActionRed); @@ -3217,7 +3217,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) return(AI_ACTION_RED_ALERT); } } - // if we can't call in artillery, jam frequencies, so that the palyer can't use radio skills + // if we can't call in artillery, jam frequencies, so that the player can't use radio skills else if (!pSoldier->IsJamming() && !pSoldier->CanAnyArtilleryStrikeBeOrdered(&tmp)) { DebugAI(AI_MSG_INFO, pSoldier, String("Start jamming radio frequencies"), gLogDecideActionRed); @@ -3351,6 +3351,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) DebugAI(AI_MSG_TOPIC, pSoldier, String("[Provide medical aid]"), gLogDecideActionRed); UINT8 ubPerson = GetClosestWoundedSoldierID( pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius, pSoldier->bTeam); + // are we ourselves the patient? if ( ubPerson == pSoldier->ubID ) { @@ -3418,6 +3419,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) DebugAI(AI_MSG_TOPIC, pSoldier, String("[Seek medical aid]"), gLogDecideActionRed); UINT8 ubPerson = GetClosestMedicSoldierID( pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius / 2, pSoldier->bTeam); + if ( ubPerson != NOBODY ) { DebugAI(AI_MSG_INFO, pSoldier, String("Found a medic!"), gLogDecideActionRed); @@ -3494,7 +3496,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } } - //////////////////////////////////////////////////////////////////////// // RED RETREAT //////////////////////////////////////////////////////////////////////// @@ -3608,6 +3609,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // (we never want NPCs to choose to radio if they would have to wait a turn) if ( !(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT) && !fCivilian && (pSoldier->bActionPoints >= APBPConstants[AP_RADIO]) && (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1) ) { + DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: checking to radio red alert"); DebugAI(AI_MSG_TOPIC, pSoldier, String("[Radio red alert]"), gLogDecideActionRed); @@ -3747,7 +3749,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) else { DebugAI(AI_MSG_INFO, pSoldier, String("Throw not possible"), gLogDecideActionRed); } } - // sevenfm: no Main Red AI for civilians if ( (gGameExternalOptions.fEnemyTanksCanMoveInTactical || !ARMED_VEHICLE( pSoldier )) && !(pSoldier->flags.uiStatusFlags & (SOLDIER_DRIVER | SOLDIER_PASSENGER)) && @@ -4247,7 +4248,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) DebugAI(AI_MSG_INFO, pSoldier, String("Distance not suitable, seek enemy instead"), gLogDecideActionRed); return AI_ACTION_SEEK_OPPONENT; } - pSoldier->aiData.usActionData = FindFlankingSpot (pSoldier, sClosestDisturbance, action ); if (TileIsOutOfBounds(pSoldier->aiData.usActionData) || pSoldier->numFlanks >= MAX_FLANKS_RED ) @@ -4268,6 +4268,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) return(AI_ACTION_SEEK_OPPONENT); } } + else { DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy"), gLogDecideActionRed); @@ -4582,7 +4583,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) DebugAI(AI_MSG_INFO, pSoldier, String("Soldier is shocked, attempt to run away"), gLogDecideActionRed); // look for best place to RUN AWAY to (farthest from the closest threat) pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); - + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { #ifdef DEBUGDECISIONS @@ -5425,7 +5426,6 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) (!ProneSightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE) && !AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || pSoldier->TakenLargeHit()) && (pSoldier->TakenLargeHit() || pSoldier->ShockLevelPercent() > 20 + Random(80))) { - DebugAI(AI_MSG_TOPIC, pSoldier, String("[Self smoke when under fire]")); DebugAI(AI_MSG_INFO, pSoldier, String("check if soldier can cover himself with smoke")); CheckTossSelfSmoke(pSoldier, &BestThrow); From 1a984201600baebd36fa7ff1f40953f24eedb997 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sat, 10 Feb 2024 18:56:11 +0200 Subject: [PATCH 43/80] Remove extraneous check Allow at least considering throwing smoke for cover even if pSoldier has used some AP from its initial amount. --- TacticalAI/DecideAction.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index bb3fbd013..0aa9100e7 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -6187,7 +6187,6 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) // check path to closest disturbance if (gfTurnBasedAI && pSoldier->bActionPoints >= APBPConstants[AP_MINIMUM] && - pSoldier->bActionPoints == pSoldier->bInitialActionPoints && !TileIsOutOfBounds(sClosestDisturbance) && RangeChangeDesire(pSoldier) > 3 && !AICheckIsSniper(pSoldier) && From 67a26e09362576cbbe1f0c1edd88fb209a7934cf Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 11 Feb 2024 11:21:41 +0200 Subject: [PATCH 44/80] Add more AI logging --- TacticalAI/DecideAction.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 0aa9100e7..8af840007 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -5253,6 +5253,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) AIPopMessage(tempstr); #endif + DebugAI(AI_MSG_INFO, pSoldier, String("Gassed or low on breath, run away to grid %d", pSoldier->aiData.usActionData)); return(AI_ACTION_RUN_AWAY); } } @@ -5563,6 +5564,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) if ( !TileIsOutOfBounds( pSoldier->aiData.usActionData ) ) { + DebugAI(AI_MSG_INFO, pSoldier, String("[VIP Retreat] grid# %d", pSoldier->aiData.usActionData)); return AI_ACTION_RUN_AWAY; } } From dfc17c68c66dbb6cda84c002f566076a2270280b Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 11 Feb 2024 13:32:17 +0200 Subject: [PATCH 45/80] Cancel action if new situation arises Lot of actions are liable to deadlock if this is not done --- TacticalAI/AIMain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 73d768a27..30e63e955 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -776,7 +776,7 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named // Prevent AI deadlocking in case enemy is performing an action and player gets an interrupt. // Without this, if player doesn't move any mercs, the AI soldier will wait until the deadlock is broken. // By canceling the AI action, the AI can then reconsider actions. - if (pSoldier->aiData.bAction == AI_ACTION_FIRE_GUN || pSoldier->aiData.bAction == AI_ACTION_KNIFE_MOVE || pSoldier->aiData.bAction == AI_ACTION_STEAL_MOVE) + //if (pSoldier->aiData.bAction == AI_ACTION_FIRE_GUN || pSoldier->aiData.bAction == AI_ACTION_KNIFE_MOVE || pSoldier->aiData.bAction == AI_ACTION_STEAL_MOVE || pSoldier->aiData.bAction == AI_ACTION_KNIFE_STAB) { DebugAI(AI_MSG_INFO, pSoldier, String("New Situation")); CancelAIAction(pSoldier, FALSE); From 57716e1c07067c276904d89909158a0e78b22f5d Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 11 Feb 2024 17:20:10 +0200 Subject: [PATCH 46/80] Revert back to original AI deadlock breaking For now.. --- TacticalAI/AIMain.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 30e63e955..76e58b3a3 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -799,7 +799,7 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named ********/ if (gfTurnBasedAI) { - if (true) +#if 0 { // added by Flugente: static pointers, used to break out of an endless circles static SOLDIERTYPE* pLastDecisionSoldier = NULL; @@ -832,7 +832,7 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named lastdecisioncount = 0; } } - else +#else { time_t tCurrentTime = time(0); UINT32 uiShortDelay = 10; @@ -871,6 +871,7 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named #endif } } +#endif } // We STILL do not want to issue new orders while an attack busy situation is going on. This can happen, for example, From ab2c5fc10db3fb9d2187ac0df82c3d3324fe9ced Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:03:43 +0200 Subject: [PATCH 47/80] Add BOXER() & ISVIP() macros --- Tactical/Boxing.cpp | 11 ++++++----- Tactical/Interface.cpp | 2 +- Tactical/Overhead.cpp | 18 +++++++++--------- Tactical/Soldier Ani.cpp | 2 +- Tactical/Soldier Control.cpp | 2 +- Tactical/Soldier macros.h | 4 +++- Tactical/Weapons.cpp | 12 ++++++------ TacticalAI/AIList.cpp | 3 ++- TacticalAI/AIUtils.cpp | 10 +++++----- TacticalAI/DecideAction.cpp | 31 ++++++++++++++----------------- 10 files changed, 48 insertions(+), 47 deletions(-) diff --git a/Tactical/Boxing.cpp b/Tactical/Boxing.cpp index 02db51b6d..05a4b4a08 100644 --- a/Tactical/Boxing.cpp +++ b/Tactical/Boxing.cpp @@ -22,6 +22,7 @@ #include "Font Control.h" #include "message.h" #include "GameSettings.h" // added by SANDRO + #include "Soldier macros.h" INT32 gsBoxerGridNo[ NUM_BOXERS ] = { 11393, 11233, 11073 }; UINT8 gubBoxerID[ NUM_BOXERS ] = { NOBODY, NOBODY, NOBODY }; @@ -55,7 +56,7 @@ void ExitBoxing( void ) if ( pSoldier != NULL ) { - if ( ( pSoldier->flags.uiStatusFlags & SOLDIER_BOXER ) && InARoom( pSoldier->sGridNo, &usRoom ) && usRoom == BOXING_RING ) + if ( BOXER(pSoldier) && InARoom( pSoldier->sGridNo, &usRoom ) && usRoom == BOXING_RING ) { if ( pSoldier->flags.uiStatusFlags & SOLDIER_PC ) { @@ -239,7 +240,7 @@ void CountPeopleInBoxingRingAndDoActions( void ) { ++ubPlayersInRing; - if ( !pNonBoxingPlayer && !(pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) ) + if ( !pNonBoxingPlayer && !BOXER(pSoldier) ) { pNonBoxingPlayer = pSoldier; } @@ -291,7 +292,7 @@ void CountPeopleInBoxingRingAndDoActions( void ) // ladieees and gennleman, we have a fight! for (uiLoop = 0; uiLoop < 2; ++uiLoop) { - if (!(pInRing[uiLoop]->flags.uiStatusFlags & SOLDIER_BOXER)) + if (!BOXER(pInRing[uiLoop])) { // set as boxer! pInRing[uiLoop]->flags.uiStatusFlags |= SOLDIER_BOXER; @@ -515,7 +516,7 @@ void BoxingMovementCheck( SOLDIERTYPE * pSoldier ) // someone moving in/into the ring CountPeopleInBoxingRingAndDoActions(); } - else if ( ( gTacticalStatus.bBoxingState == BOXING ) && ( pSoldier->flags.uiStatusFlags & SOLDIER_BOXER ) ) + else if ( ( gTacticalStatus.bBoxingState == BOXING ) && BOXER(pSoldier) ) { // boxer stepped out of the ring! BoxingPlayerDisqualified( pSoldier, BOXER_OUT_OF_RING ); @@ -565,7 +566,7 @@ void ClearAllBoxerFlags( void ) { for (UINT32 uiSlot = 0; uiSlot < guiNumMercSlots; ++uiSlot) { - if ( MercSlots[ uiSlot ] && MercSlots[ uiSlot ]->flags.uiStatusFlags & SOLDIER_BOXER ) + if ( MercSlots[ uiSlot ] && BOXER(MercSlots[ uiSlot ]) ) { // Flugente: nuke the entire opponent count, remove boxing flag, reevaluate opponent list DecayIndividualOpplist(MercSlots[uiSlot]); diff --git a/Tactical/Interface.cpp b/Tactical/Interface.cpp index 5ab367503..42f3e9d0b 100644 --- a/Tactical/Interface.cpp +++ b/Tactical/Interface.cpp @@ -6299,7 +6299,7 @@ BOOLEAN ShowSoldierRoleSymbol(SOLDIERTYPE* pSoldier) if ( pSoldier->usSkillCounter[SOLDIER_COUNTER_ROLE_OBSERVED] >= gGameExternalOptions.usTurnsToUncover ) { // are we a VIP? show that only when the player knows a VIP is in this sector. otherwise, don't even show our officer property - if ( pSoldier->usSoldierFlagMask & SOLDIER_VIP && !pSoldier->bSectorZ ) + if (ISVIP(pSoldier) && !pSoldier->bSectorZ ) { if ( PlayerKnowsAboutVIP( pSoldier->sSectorX, pSoldier->sSectorY ) ) { diff --git a/Tactical/Overhead.cpp b/Tactical/Overhead.cpp index 3052c2bb1..2a85df93e 100644 --- a/Tactical/Overhead.cpp +++ b/Tactical/Overhead.cpp @@ -3043,7 +3043,7 @@ BOOLEAN HandleAtNewGridNo( SOLDIERTYPE *pSoldier, BOOLEAN *pfKeepMoving ) // sevenfm: check all nearby enemy boxers for opportunity attack if (IS_MERC_BODY_TYPE(pSoldier) && - (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && + BOXER(pSoldier) && gTacticalStatus.bBoxingState == BOXING && pSoldier->aiData.bAlertStatus >= STATUS_RED) { @@ -3064,7 +3064,7 @@ BOOLEAN HandleAtNewGridNo( SOLDIERTYPE *pSoldier, BOOLEAN *pfKeepMoving ) pOpponent->bCollapsed || pOpponent->bBreathCollapsed || !IS_MERC_BODY_TYPE(pOpponent) || - !(pOpponent->flags.uiStatusFlags & SOLDIER_BOXER) || + !BOXER(pOpponent) || gAnimControl[pOpponent->usAnimState].ubEndHeight < ANIM_STAND || pOpponent->pathing.bLevel != pSoldier->pathing.bLevel || !SoldierToSoldierLineOfSightTest(pOpponent, pSoldier, TRUE, CALC_FROM_WANTED_DIR) || @@ -7233,7 +7233,7 @@ void RemoveCapturedEnemiesFromSectorInfo( INT16 sMapX, INT16 sMapY, INT8 bMapZ ) //if ( pTeamSoldier->stats.bLife >= OKLIFE && pTeamSoldier->stats.bLife != 0 ) { // officers and generals are 'special' prisoners... - if ( pTeamSoldier->usSoldierFlagMask & SOLDIER_VIP ) + if (ISVIP(pTeamSoldier)) ++sNumPrisoner[PRISONER_GENERAL]; // downed pilots count as officers too, even though they are civilians. This makes capturing them more rewarding else if ( (pTeamSoldier->usSoldierFlagMask & SOLDIER_ENEMY_OFFICER) || pTeamSoldier->ubCivilianGroup == DOWNEDPILOT_CIV_GROUP ) @@ -7262,7 +7262,7 @@ void RemoveCapturedEnemiesFromSectorInfo( INT16 sMapX, INT16 sMapY, INT8 bMapZ ) } // Flugente: VIPs - if ( pTeamSoldier->usSoldierFlagMask & SOLDIER_VIP ) + if (ISVIP(pTeamSoldier)) DeleteVIP( pTeamSoldier->sSectorX, pTeamSoldier->sSectorY ); // Flugente: turncoats @@ -9320,10 +9320,10 @@ BOOLEAN ProcessImplicationsOfPCAttack( SOLDIERTYPE * pSoldier, SOLDIERTYPE ** pp if ( gTacticalStatus.bBoxingState == BOXING ) { // should have a check for "in boxing ring", no? - if ( ( pSoldier->usAttackingWeapon != NOTHING && !Item[pSoldier->usAttackingWeapon].brassknuckles ) || !( pSoldier->flags.uiStatusFlags & SOLDIER_BOXER ) || pSoldier->IsRiotShieldEquipped() ) + if ( ( pSoldier->usAttackingWeapon != NOTHING && !Item[pSoldier->usAttackingWeapon].brassknuckles ) || !BOXER(pSoldier) || pSoldier->IsRiotShieldEquipped() ) { // someone's cheating! - if ( (Item[ pSoldier->usAttackingWeapon ].usItemClass == IC_BLADE || Item[ pSoldier->usAttackingWeapon ].usItemClass == IC_PUNCH) && (pTarget->flags.uiStatusFlags & SOLDIER_BOXER) ) + if ( (Item[ pSoldier->usAttackingWeapon ].usItemClass == IC_BLADE || Item[ pSoldier->usAttackingWeapon ].usItemClass == IC_PUNCH) && BOXER(pTarget) ) { // knife or brass knuckles disqualify the player! BoxingPlayerDisqualified( pSoldier, BAD_ATTACK ); @@ -9334,7 +9334,7 @@ BOOLEAN ProcessImplicationsOfPCAttack( SOLDIERTYPE * pSoldier, SOLDIERTYPE ** pp //gTacticalStatus.bBoxingState = NOT_BOXING; SetBoxingState( NOT_BOXING ); // if we are attacking a boxer we should set them to neutral (temporarily) so that the rest of the civgroup code works... - if ( (pTarget->bTeam == CIV_TEAM) && (pTarget->flags.uiStatusFlags & SOLDIER_BOXER) ) + if ( (pTarget->bTeam == CIV_TEAM) && BOXER(pTarget) ) { SetSoldierNeutral( pTarget ); } @@ -9481,7 +9481,7 @@ BOOLEAN ProcessImplicationsOfPCAttack( SOLDIERTYPE * pSoldier, SOLDIERTYPE ** pp //TriggerNPCWithIHateYouQuote( pTarget->ubProfile ); } } - else if ( pTarget->ubCivilianGroup != NON_CIV_GROUP && !( pTarget->flags.uiStatusFlags & SOLDIER_BOXER ) ) + else if ( pTarget->ubCivilianGroup != NON_CIV_GROUP && !BOXER(pTarget) ) { // Firing at a civ in a civ group who isn't hostile... if anyone in that civ group can see this // going on they should become hostile. @@ -10927,7 +10927,7 @@ void TurnCoatAttemptMessageBoxCallBack( UINT8 ubExitValue ) UINT8 approachchance = MercPtrs[gusSelectedSoldier]->GetTurncoatConvinctionChance( prisonerdialoguetargetID, approachselected ); // you can never turn a VIP (though we don't tell the player if someone is a VIP, lest they have an exploit to find out) - if ( pSoldier->usSoldierFlagMask & SOLDIER_VIP ) + if (ISVIP(pSoldier)) approachchance = 0; // as using random numbers to pass the check would result in players savescumming, use a number based on the soldier's stats diff --git a/Tactical/Soldier Ani.cpp b/Tactical/Soldier Ani.cpp index eda237d26..ad304f876 100644 --- a/Tactical/Soldier Ani.cpp +++ b/Tactical/Soldier Ani.cpp @@ -4234,7 +4234,7 @@ BOOLEAN HandleSoldierDeath( SOLDIERTYPE *pSoldier , BOOLEAN *pfMadeCorpse ) } // Flugente: VIPs - if ( pSoldier->usSoldierFlagMask & SOLDIER_VIP ) + if (ISVIP(pSoldier)) { DeleteVIP( pSoldier->sSectorX, pSoldier->sSectorY ); } diff --git a/Tactical/Soldier Control.cpp b/Tactical/Soldier Control.cpp index dacb8dfc8..b86d1483a 100644 --- a/Tactical/Soldier Control.cpp +++ b/Tactical/Soldier Control.cpp @@ -9847,7 +9847,7 @@ void SOLDIERTYPE::BeginSoldierGetup( void ) else { this->bTurnsCollapsed++; - if ( (gTacticalStatus.bBoxingState == BOXING) && (this->flags.uiStatusFlags & SOLDIER_BOXER) ) + if ( (gTacticalStatus.bBoxingState == BOXING) && (BOXER(this)) ) { if ( this->bTurnsCollapsed > 1 ) { diff --git a/Tactical/Soldier macros.h b/Tactical/Soldier macros.h index af0a32fe3..f31c83fff 100644 --- a/Tactical/Soldier macros.h +++ b/Tactical/Soldier macros.h @@ -37,8 +37,10 @@ #define TANK( p ) (p->ubBodyType == TANK_NE || p->ubBodyType == TANK_NW ) #define ENEMYROBOT( p ) (p->ubBodyType == ROBOTNOWEAPON && p->bTeam == ENEMY_TEAM) #define ARMED_VEHICLE( p ) ( TANK( p ) || COMBAT_JEEP(p) ) +#define BOXER( p ) ( p->flags.uiStatusFlags & SOLDIER_BOXER ) +#define ISVIP( p ) ( p->usSoldierFlagMask & SOLDIER_VIP ) //#define OK_ENTERABLE_VEHICLE( p ) ( ( p->flags.uiStatusFlags & SOLDIER_VEHICLE ) && !TANK( p ) && p->stats.bLife >= OKLIFE ) #define OK_ENTERABLE_VEHICLE( p ) ( ( p->flags.uiStatusFlags & SOLDIER_VEHICLE ) && (!ARMED_VEHICLE( p ) || !(p->flags.uiStatusFlags & SOLDIER_ENEMY) ) && p->stats.bLife >= OKLIFE ) -#endif \ No newline at end of file +#endif diff --git a/Tactical/Weapons.cpp b/Tactical/Weapons.cpp index cdc60e02a..f421763f7 100644 --- a/Tactical/Weapons.cpp +++ b/Tactical/Weapons.cpp @@ -4260,10 +4260,10 @@ BOOLEAN UseHandToHand( SOLDIERTYPE *pSoldier, INT32 sTargetGridNo, BOOLEAN fStea // sevenfm: bonus for boxers for attack from the back if (iHitChance < 100 && - (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && + BOXER(pSoldier) && !pSoldier->bBlindedCounter && gAnimControl[pTargetSoldier->usAnimState].ubEndHeight > ANIM_PRONE && - (pTargetSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && + BOXER(pTargetSoldier) && pTargetSoldier->usSoldierFlagMask2 & SOLDIER_BACK_ATTACK) { iHitChance += (100 - iHitChance) / 2; @@ -4879,7 +4879,7 @@ BOOLEAN UseHandToHand( SOLDIERTYPE *pSoldier, INT32 sTargetGridNo, BOOLEAN fStea if (pTargetSoldier->bActionPoints > 0 && gGameOptions.fNewTraitSystem && gTacticalStatus.bBoxingState == BOXING && - (pTargetSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && + BOXER(pTargetSoldier) && Chance(ubCounterattackChance) && IS_MERC_BODY_TYPE(pSoldier) && IS_MERC_BODY_TYPE(pTargetSoldier) && @@ -9719,7 +9719,7 @@ UINT32 CalcChanceHTH( SOLDIERTYPE * pAttacker,SOLDIERTYPE *pDefender, INT16 ubAi { // Changed from DG by CJC to give higher chances of hitting with a stab or punch // sevenfm: lowered chance for boxers - if (pAttacker->flags.uiStatusFlags & SOLDIER_BOXER) + if (BOXER(pAttacker)) iChance = 50 + (iAttRating - iDefRating) / 3; else iChance = 67 + (iAttRating - iDefRating) / 3; @@ -9774,8 +9774,8 @@ UINT32 CalcChanceHTH( SOLDIERTYPE * pAttacker,SOLDIERTYPE *pDefender, INT16 ubAi // sevenfm: bonus for boxers for attacking from the back if (ubMode == HTH_MODE_PUNCH && - (pAttacker->flags.uiStatusFlags & SOLDIER_BOXER) && - (pDefender->flags.uiStatusFlags & SOLDIER_BOXER) && + BOXER(pAttacker) && + BOXER(pDefender) && iChance < 100 && !pAttacker->bBlindedCounter && gAnimControl[pDefender->usAnimState].ubEndHeight > ANIM_PRONE && diff --git a/TacticalAI/AIList.cpp b/TacticalAI/AIList.cpp index fef94af55..8913f71f5 100644 --- a/TacticalAI/AIList.cpp +++ b/TacticalAI/AIList.cpp @@ -18,6 +18,7 @@ #include "opplist.h" #include "Interface.h" #include "Tactical Save.h" +#include #define AI_LIST_SIZE TOTAL_SOLDIERS @@ -180,7 +181,7 @@ BOOLEAN InsertIntoAIList( UINT8 ubID, INT8 bPriority ) BOOLEAN SatisfiesAIListConditions( SOLDIERTYPE * pSoldier, UINT8 * pubDoneCount, BOOLEAN fDoRandomChecks ) { - if ( (gTacticalStatus.bBoxingState == BOXING) && !(pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) ) + if ( (gTacticalStatus.bBoxingState == BOXING) && !BOXER(pSoldier) ) { return( FALSE ); } diff --git a/TacticalAI/AIUtils.cpp b/TacticalAI/AIUtils.cpp index 1f55af6c2..e76d86df8 100644 --- a/TacticalAI/AIUtils.cpp +++ b/TacticalAI/AIUtils.cpp @@ -418,7 +418,7 @@ UINT16 DetermineMovementMode( SOLDIERTYPE * pSoldier, INT8 bAction ) if (IS_MERC_BODY_TYPE(pSoldier) && pSoldier->aiData.bAlertStatus >= STATUS_YELLOW && !InWaterGasOrSmoke(pSoldier, pSoldier->sGridNo) && - !(pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && + !BOXER(pSoldier) && !TileIsOutOfBounds(sClosestThreat) && (pSoldier->bTeam == ENEMY_TEAM || pSoldier->bTeam == MILITIA_TEAM)) { @@ -1474,7 +1474,7 @@ INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLev if (sClosestOpponent == NOWHERE || iRange < iClosestRange || - pClosestOpponent && !pClosestOpponent->IsZombie() && !(pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && pClosestOpponent->stats.bLife < OKLIFE && pOpponent->stats.bLife >= OKLIFE) + pClosestOpponent && !pClosestOpponent->IsZombie() && !BOXER(pSoldier) && pClosestOpponent->stats.bLife < OKLIFE && pOpponent->stats.bLife >= OKLIFE) { iClosestRange = iRange; sClosestOpponent = sGridNo; @@ -2670,7 +2670,7 @@ INT32 CalcManThreatValue( SOLDIERTYPE *pEnemy, INT32 sMyGrid, UINT8 ubReduceForC } // in boxing mode, let only a boxer be considered a threat. - if ( (gTacticalStatus.bBoxingState == BOXING) && !(pEnemy->flags.uiStatusFlags & SOLDIER_BOXER) ) + if ( (gTacticalStatus.bBoxingState == BOXING) && !BOXER(pEnemy) ) { iThreatValue = -999; return( iThreatValue ); @@ -4803,7 +4803,7 @@ BOOLEAN ValidOpponent(SOLDIERTYPE* pSoldier, SOLDIERTYPE* pOpponent) pSoldier->bSide == pOpponent->bSide || pSoldier->aiData.bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != SLAY || (pOpponent->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle(pOpponent->bVehicleID) == 0) || - gTacticalStatus.bBoxingState == BOXING && (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && !(pOpponent->flags.uiStatusFlags & SOLDIER_BOXER) || + gTacticalStatus.bBoxingState == BOXING && BOXER(pSoldier) && !BOXER(pOpponent) || pOpponent->ubBodyType == CROW) { return FALSE; @@ -4913,7 +4913,7 @@ BOOLEAN SoldierAI(SOLDIERTYPE *pSoldier) if (!IS_MERC_BODY_TYPE(pSoldier) || pSoldier->aiData.bNeutral || fCivilian || - pSoldier->flags.uiStatusFlags & SOLDIER_BOXER || + BOXER(pSoldier) || ARMED_VEHICLE(pSoldier) || pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE || AM_A_ROBOT(pSoldier) || diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 8af840007..d2d1e840d 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -718,7 +718,7 @@ INT8 DecideActionGreen(SOLDIERTYPE *pSoldier) if ( gTacticalStatus.bBoxingState != NOT_BOXING ) { - if (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) + if (BOXER(pSoldier)) { if ( gTacticalStatus.bBoxingState == PRE_BOXING ) { @@ -2707,7 +2707,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) !bInGas && pSoldier->CheckInitialAP() && !pSoldier->IsFlanking() && - !(pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && + !BOXER(pSoldier) && (CanNPCAttack(pSoldier) == TRUE)) { BestThrow.ubPossible = FALSE; // by default, assume Throwing isn't possible @@ -5195,7 +5195,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) } INT8 bInWater, bInDeepWater, bInGas; - if ( pSoldier->flags.uiStatusFlags & SOLDIER_BOXER ) + if (BOXER(pSoldier) ) { if ( gTacticalStatus.bBoxingState == PRE_BOXING ) { @@ -5304,7 +5304,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) { bCanAttack = FALSE; } - else if (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) + else if (BOXER(pSoldier)) { bCanAttack = TRUE; fTryPunching = TRUE; @@ -5318,7 +5318,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) { if (fCivilian) { - if ( ( bCanAttack == NOSHOOT_NOWEAPON) && !(pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && pSoldier->ubBodyType != COW && pSoldier->ubBodyType != CRIPPLECIV && !(pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE) ) + if ( ( bCanAttack == NOSHOOT_NOWEAPON) && !BOXER(pSoldier) && pSoldier->ubBodyType != COW && pSoldier->ubBodyType != CRIPPLECIV && !(pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE) ) { // cower in fear!! if ( pSoldier->flags.uiStatusFlags & SOLDIER_COWERING ) @@ -5806,7 +5806,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) if (BestStab.ubPossible) { - if (!(pSoldier->flags.uiStatusFlags & SOLDIER_BOXER)) + if (!BOXER(pSoldier)) { // if we have not enough APs to deal at least two or three punches, // reduce the attack value as one punch ain't much @@ -5903,7 +5903,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) // cautious boxer approach, reserve AP for two attacks (only if not attacking from the back) if (BestStab.ubPossible && - (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && + BOXER(pSoldier) && SpacesAway(pSoldier->sGridNo, BestStab.sTarget) > 2 && BestStab.ubOpponent != NOBODY && MercPtrs[BestStab.ubOpponent] && @@ -5920,7 +5920,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) // try to avoid frontal attack if (BestStab.ubPossible && - (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && + BOXER(pSoldier) && SpacesAway(pSoldier->sGridNo, BestStab.sTarget) > 1 && BestStab.ubOpponent != NOBODY && MercPtrs[BestStab.ubOpponent] && @@ -6285,8 +6285,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) { DebugAI(AI_MSG_TOPIC, pSoldier, String("[Allow taking cover]")); // okay, really got to wonder about this... could taking cover be an option? - if (ubCanMove && pSoldier->aiData.bOrders != STATIONARY && !gfHiddenInterrupt && - !(pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) ) + if (ubCanMove && pSoldier->aiData.bOrders != STATIONARY && !gfHiddenInterrupt && !BOXER(pSoldier) ) { // make militia a bit more cautious // 3 (UINT16) CONVERSIONS HERE TO AVOID ERRORS. GOTTHARD 7/15/08 @@ -6323,7 +6322,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) if ( (ubCanMove && !SkipCoverCheck && !gfHiddenInterrupt && ((ubBestAttackAction == AI_ACTION_NONE) || pSoldier->aiData.bLastAttackHit) && (pSoldier->bTeam != gbPlayerNum || pSoldier->aiData.fAIFlags & AI_RTP_OPTION_CAN_SEEK_COVER) && - !(pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) ) + !BOXER(pSoldier) ) || fAllowCoverCheck ) { DebugAI(AI_MSG_TOPIC, pSoldier, String("[Find cover]")); @@ -7004,7 +7003,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) ////////////////////////////////////////////////////////////////////// // BOXER CLOSE IN ON OPPONENT ////////////////////////////////////////////////////////////////////// - if (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER ) + if (BOXER(pSoldier)) { DebugAI(AI_MSG_TOPIC, pSoldier, String("[Make boxer close if possible]")); @@ -10071,8 +10070,7 @@ INT8 ArmedVehicleDecideActionBlack( SOLDIERTYPE *pSoldier ) (RangeChangeDesire( pSoldier ) >= 4) ) { // okay, really got to wonder about this... could taking cover be an option? - if ( ubCanMove && pSoldier->aiData.bOrders != STATIONARY && !gfHiddenInterrupt && - !(pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) ) + if ( ubCanMove && pSoldier->aiData.bOrders != STATIONARY && !gfHiddenInterrupt && !BOXER(pSoldier) ) { // make militia a bit more cautious // 3 (UINT16) CONVERSIONS HERE TO AVOID ERRORS. GOTTHARD 7/15/08 @@ -10103,9 +10101,8 @@ INT8 ArmedVehicleDecideActionBlack( SOLDIERTYPE *pSoldier ) if ( (ubCanMove && !SkipCoverCheck && !gfHiddenInterrupt && ((ubBestAttackAction == AI_ACTION_NONE) || pSoldier->aiData.bLastAttackHit) && - (pSoldier->bTeam != gbPlayerNum || pSoldier->aiData.fAIFlags & AI_RTP_OPTION_CAN_SEEK_COVER) && - !(pSoldier->flags.uiStatusFlags & SOLDIER_BOXER)) - || fAllowCoverCheck ) + (pSoldier->bTeam != gbPlayerNum || pSoldier->aiData.fAIFlags & AI_RTP_OPTION_CAN_SEEK_COVER) && !BOXER(pSoldier)) || + fAllowCoverCheck ) { sBestCover = FindBestNearbyCover( pSoldier, pSoldier->aiData.bAIMorale, &iCoverPercentBetter ); } From de91c7a9fdbdd0e57f99e19d393c8f804d666908 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:30:46 +0200 Subject: [PATCH 48/80] Render debug info in tactical Functionality for rendering pathfinding and cover values in tactical was behind defunct #ifdefs. It's now compiled always, but only enabled if cheat level is BEDUG_CHEAT_LEVEL. Unified rendering the debug info into one function, instead of several nearly identical ones. Ctrl + Shift + Z cycles through the different debug modes. Now it should be fairly easy to add more info modes like these two in the future. --- Tactical/Handle UI.cpp | 53 +++++++- Tactical/Handle UI.h | 4 +- Tactical/PATHAI.cpp | 135 +++++-------------- Tactical/Turn Based Input.cpp | 25 ++++ TacticalAI/FindLocations.cpp | 63 +++------ TacticalAI/ai.h | 4 - TileEngine/renderworld.cpp | 235 +++++++++++++++++----------------- TileEngine/renderworld.h | 12 +- TileEngine/worlddef.cpp | 14 +- 9 files changed, 262 insertions(+), 283 deletions(-) diff --git a/Tactical/Handle UI.cpp b/Tactical/Handle UI.cpp index e42433812..9aa9fc8fe 100644 --- a/Tactical/Handle UI.cpp +++ b/Tactical/Handle UI.cpp @@ -87,7 +87,7 @@ #include "SaveLoadScreen.h" #include "Map Screen Interface.h" // added by Flugente for SquadNames #include "Keys.h" // added by silversurfer for door handling from the side - +#include "Cheats.h" #include "AIInternals.h" extern BOOLEAN gubWorldTileInLight[MAX_ALLOWED_WORLD_MAX]; extern BOOLEAN gubIsCorpseThere[MAX_ALLOWED_WORLD_MAX]; @@ -386,7 +386,7 @@ BOOLEAN gfDisplayTimerCursor = FALSE; UINT32 guiTimerCursorID = 0; UINT32 guiTimerLastUpdate = 0; UINT32 guiTimerCursorDelay = 0; - +UINT8 gRenderDebugInfoMode = DEBUG_OFF; CHAR16 gzLocation[ 20 ]; BOOLEAN gfLocation = FALSE; @@ -518,6 +518,54 @@ void GetMercOknoDirection( UINT8 ubSoldierID, BOOLEAN *pfGoDown, BOOLEAN *pfGoUp } //---------------------------------------------------------------------------------- +void HandleRenderDebugInfoModes() +{ + if (DEBUG_CHEAT_LEVEL()) + { + switch (gRenderDebugInfoMode) + { + case DEBUG_PATHFINDING: + // Nothing to do here, pathfinding info is filled in the pathing functions. + break; + case DEBUG_THREATVALUE: + break; + case DEBUG_COVERVALUE: + // Calculate cover values for pSoldier under cursor, or for currently selected merc, if nobody is under the cursor. + if (gTacticalStatus.Team[OUR_TEAM].bTeamActive) + { + static SOLDIERTYPE* previousSoldier = nullptr; + static INT32 previousLocation = NOWHERE; + static UINT8 previousStance = 0; + + UINT16 usSoldierIndex = NOBODY; + UINT32 uiMercFlags; + FindSoldierFromMouse(&usSoldierIndex, &uiMercFlags); + if (usSoldierIndex == NOBODY) + { + usSoldierIndex = gusSelectedSoldier; + } + + if (usSoldierIndex != NOBODY) + { + // Get Soldier + INT32 iPercentBetter; + SOLDIERTYPE* pSoldier; + GetSoldier(&pSoldier, usSoldierIndex); + if (previousSoldier != pSoldier || previousLocation != pSoldier->sGridNo || previousStance != gAnimControl[pSoldier->usAnimState].ubEndHeight) + { + FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iPercentBetter); + previousSoldier = pSoldier; + previousLocation = pSoldier->sGridNo; + previousStance = gAnimControl[pSoldier->usAnimState].ubEndHeight; + } + } + } + break; + default: // off + break; + } + } +} void PreventFromTheFreezingBug(SOLDIERTYPE* pSoldier) { @@ -691,6 +739,7 @@ UINT32 HandleTacticalUI( void ) } } + HandleRenderDebugInfoModes(); // Check if current event has changed and clear event if so, to prepare it for execution // Clearing it does things like set first time flag, param variables, etc if ( uiNewEvent != guiOldEvent ) diff --git a/Tactical/Handle UI.h b/Tactical/Handle UI.h index 5937d9112..bdcc6a1f1 100644 --- a/Tactical/Handle UI.h +++ b/Tactical/Handle UI.h @@ -296,6 +296,8 @@ extern BOOLEAN gfUIForceReExamineCursorData; extern INT16 guiCreateGuyIndex; extern INT16 guiCreateBadGuyIndex; +extern UINT8 gRenderDebugInfoMode; + // WANNE: Calculate the APs to turn around INT16 APsToTurnAround(SOLDIERTYPE *pSoldier, INT32 sAdjustedGridNo); @@ -382,4 +384,4 @@ void GetGridNoScreenXY( INT32 sGridNo, INT16 *pScreenX, INT16 *pScreenY ); //Legion by Jazz void GetMercOknoDirection( UINT8 ubSoldierID, BOOLEAN *pfGoDown, BOOLEAN *pfGoUp ); -#endif \ No newline at end of file +#endif diff --git a/Tactical/PATHAI.cpp b/Tactical/PATHAI.cpp index 02a8067d3..882c6e471 100644 --- a/Tactical/PATHAI.cpp +++ b/Tactical/PATHAI.cpp @@ -25,7 +25,7 @@ #include "english.h" #include "worlddef.h" #include "worldman.h" -// #include "renderworld.h" + #include "renderworld.h" #include "pathai.h" #include "Points.h" #include "ai.h" @@ -41,7 +41,7 @@ #include "Rotting Corpses.h" #include "Meanwhile.h" #include "connect.h" - +#include #include "LOS.h" //ddd //forward declarations of common classes to eliminate includes @@ -66,19 +66,8 @@ extern BOOLEAN InGasSpot(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bLevel); // skiplist has extra level of pointers every 4 elements, so a level 5is optimized for // 4 to the power of 5 elements, or 2 to the power of 10, 1024 -//#define PATHAI_VISIBLE_DEBUG //#define PATHAI_SKIPLIST_DEBUG - -#ifdef PATHAI_VISIBLE_DEBUG - #include "video.h" - -//extern INT16 gsCoverValue[WORLD_MAX]; -extern INT16 * gsCoverValue; - BOOLEAN gfDisplayCoverValues = TRUE; - BOOLEAN gfDrawPathPoints = TRUE; -#endif - BOOLEAN gfPlotPathToExitGrid = FALSE; BOOLEAN gfRecalculatingExistingPathCost = FALSE; UINT8 gubGlobalPathFlags = 0; @@ -623,15 +612,12 @@ int AStarPathfinder::GetPath(SOLDIERTYPE *s , } - -#if defined( PATHAI_VISIBLE_DEBUG ) - if (gfDisplayCoverValues && gfDrawPathPoints) + if (gRenderDebugInfoMode == DEBUG_PATHFINDING && DEBUG_CHEAT_LEVEL()) { - memset( gsCoverValue, 0x7F, sizeof( INT16 ) * WORLD_MAX ); + ResetDebugInfoValues(); + gRenderDebugInfoValues[ StartNode ] = 0; + PATHAI_VISIBLE_DEBUG_Counter = 1; } - gsCoverValue[ StartNode ] = 0; - PATHAI_VISIBLE_DEBUG_Counter = 1; -#endif //init other private data, mostly flags endDir = lastDir = direction = startDir = 0; @@ -796,12 +782,10 @@ int AStarPathfinder::GetPath(SOLDIERTYPE *s , return 0; } -#if defined( PATHAI_VISIBLE_DEBUG ) - if (gfDisplayCoverValues && gfDrawPathPoints) + if (gRenderDebugInfoMode == DEBUG_PATHFINDING && DEBUG_CHEAT_LEVEL()) { SetRenderFlags( RENDER_FLAG_FULL ); } -#endif // Count the number of steps, but keep it less than the max path length. // Adjust the parent until it begins at the tail end of the max path length (or the dest if reachable) @@ -884,12 +868,10 @@ int AStarPathfinder::GetPath(SOLDIERTYPE *s , sizePath = giPathDataSize; } -#if defined( PATHAI_VISIBLE_DEBUG ) - if (gfDisplayCoverValues && gfDrawPathPoints) + if (gRenderDebugInfoMode == DEBUG_PATHFINDING && DEBUG_CHEAT_LEVEL()) { SetRenderFlags( RENDER_FLAG_FULL ); } -#endif #ifdef COUNT_PATHS guiSuccessfulPathChecks++; @@ -1001,15 +983,13 @@ void AStarPathfinder::ExecuteAStarLogic() SetAStarStatus(ParentNode, AStar_Closed); //ClosedList.push_back(ParentNode); -#if defined( PATHAI_VISIBLE_DEBUG ) - if (gfDisplayCoverValues && gfDrawPathPoints) + if (gRenderDebugInfoMode == DEBUG_PATHFINDING && DEBUG_CHEAT_LEVEL()) { - if (gsCoverValue[ ParentNode ] > 0) + if (gRenderDebugInfoValues[ParentNode] > 0) { - gsCoverValue[ ParentNode ] *= -1; + gRenderDebugInfoValues[ParentNode] *= -1; } } -#endif // Shouldn't G and AP be the same thing? INT16 baseGCost = GetAStarG(ParentNode); @@ -1092,7 +1072,7 @@ void AStarPathfinder::ExecuteAStarLogic() gpWorldLevelData[ CurrentNode ].ubExtFlags[0] |= MAPELEMENT_EXT_CLIMBPOINT; gpWorldLevelData[ ParentNode ].ubExtFlags[1] |= MAPELEMENT_EXT_CLIMBPOINT; #ifdef ROOF_DEBUG - gsCoverValue[CurrentNode] = 1; + gRenderDebugInfoValues[CurrentNode] = 1; #endif } @@ -1190,26 +1170,13 @@ void AStarPathfinder::ExecuteAStarLogic() int AStarH = CalcH(); int AStarF = (AStarG + extraGCoverCost) + AStarH; -#if defined( PATHAI_VISIBLE_DEBUG ) - if (gfDisplayCoverValues && gfDrawPathPoints) + if (gRenderDebugInfoMode == DEBUG_PATHFINDING && DEBUG_CHEAT_LEVEL()) { - if (gsCoverValue[CurrentNode] == 0x7F7F) + if (gRenderDebugInfoValues[CurrentNode] == 0x7FFFFFFF) { - //gsCoverValue[CurrentNode] = PATHAI_VISIBLE_DEBUG_Counter++; - gsCoverValue[CurrentNode] = (INT16) AStarF; + gRenderDebugInfoValues[CurrentNode] = (INT16) AStarF; } - /* - else if (gsCoverValue[CurrentNodeIndex] >= 0) - { - gsCoverValue[CurrentNodeIndex]++; - } - else - { - gsCoverValue[CurrentNodeIndex]--; - } - */ } -#endif //insert this node onto the heap if (GetAStarStatus(CurrentNode) == AStar_Init) @@ -2260,9 +2227,7 @@ INT32 FindBestPath(SOLDIERTYPE *s , INT32 sDestination, INT8 bLevel, INT16 usMov CHAR8 zTempString[1000], zTS[50]; #endif -#ifdef PATHAI_VISIBLE_DEBUG UINT16 usCounter = 0; -#endif fVehicle = FALSE; iOriginationX = iOriginationY = 0; @@ -2512,12 +2477,10 @@ if(!GridNoOnVisibleWorldTile(iDestination)) memset( pathQ, 0, iMaxPathQ * sizeof( path_t ) ); memset( trailTree, 0, iMaxTrailTree * sizeof( trail_t ) ); -#if defined( PATHAI_VISIBLE_DEBUG ) - if (gfDisplayCoverValues && gfDrawPathPoints) + if (gRenderDebugInfoMode == DEBUG_PATHFINDING && DEBUG_CHEAT_LEVEL()) { - memset( gsCoverValue, 0x7F, sizeof( INT16 ) * WORLD_MAX ); + ResetDebugInfoValues(); } -#endif bSkipListLevel = 1; iSkipListSize = 0; @@ -2629,15 +2592,13 @@ if(!GridNoOnVisibleWorldTile(iDestination)) // remember the cost used to get here... prevCost = gubWorldMovementCosts[ trailTree[ sCurPathNdx ].sGridNo ][ trailTree[ sCurPathNdx ].stepDir ][ bLevel ]; -#if defined( PATHAI_VISIBLE_DEBUG ) - if (gfDisplayCoverValues && gfDrawPathPoints) + if (gRenderDebugInfoMode == DEBUG_PATHFINDING && DEBUG_CHEAT_LEVEL()) { - if (gsCoverValue[ curLoc ] > 0) + if (gRenderDebugInfoValues[ curLoc ] > 0) { - gsCoverValue[ curLoc ] *= -1; + gRenderDebugInfoValues[ curLoc ] *= -1; } } -#endif /* if (fTurnSlow) @@ -3563,27 +3524,13 @@ if(!GridNoOnVisibleWorldTile(iDestination)) // costs less than the best so far to the same location? if (trailCostUsed[newLoc] != gubGlobalPathCount || newTotCost < trailCost[newLoc]) { - - #if defined( PATHAI_VISIBLE_DEBUG ) - - if (gfDisplayCoverValues && gfDrawPathPoints) - { - if (gsCoverValue[newLoc] == 0x7F7F) - { - gsCoverValue[newLoc] = usCounter++; - } - /* - else if (gsCoverValue[newLoc] >= 0) - { - gsCoverValue[newLoc]++; - } - else + if (gRenderDebugInfoMode == DEBUG_PATHFINDING && DEBUG_CHEAT_LEVEL()) + { + if (gRenderDebugInfoValues[newLoc] == 0x7FFFFFFF) { - gsCoverValue[newLoc]--; + gRenderDebugInfoValues[newLoc] = usCounter++; } - */ - } - #endif + } //NEWQUENODE; { @@ -3826,20 +3773,13 @@ if(!GridNoOnVisibleWorldTile(iDestination)) while (pathQNotEmpty && pathNotYetFound); - #if defined( PATHAI_VISIBLE_DEBUG ) - if (gfDisplayCoverValues && gfDrawPathPoints) + if (gRenderDebugInfoMode == DEBUG_PATHFINDING && DEBUG_CHEAT_LEVEL()) + { + if ( guiCurrentScreen == GAME_SCREEN ) { - SetRenderFlags( RENDER_FLAG_FULL ); - if ( guiCurrentScreen == GAME_SCREEN ) - { - RenderWorld(); - RenderCoverDebug( ); - InvalidateScreen( ); - EndFrameBufferRender(); - RefreshScreen( NULL ); - } + InvalidateRegion(gsVIEWPORT_START_X, gsVIEWPORT_START_Y, gsVIEWPORT_END_X, gsVIEWPORT_WINDOW_END_Y); } - #endif + } // work finished. Did we find a path? @@ -3894,17 +3834,10 @@ if(!GridNoOnVisibleWorldTile(iDestination)) } - #if defined( PATHAI_VISIBLE_DEBUG ) - if (gfDisplayCoverValues && gfDrawPathPoints) - { - SetRenderFlags( RENDER_FLAG_FULL ); - RenderWorld(); - RenderCoverDebug( ); - InvalidateScreen( ); - EndFrameBufferRender(); - RefreshScreen( NULL ); - } - #endif + if (gRenderDebugInfoMode == DEBUG_PATHFINDING && DEBUG_CHEAT_LEVEL()) + { + InvalidateRegion(gsVIEWPORT_START_X, gsVIEWPORT_START_Y, gsVIEWPORT_END_X, gsVIEWPORT_WINDOW_END_Y); + } // return path length : serves as a "successful" flag and a path length counter diff --git a/Tactical/Turn Based Input.cpp b/Tactical/Turn Based Input.cpp index abc773808..3e71b6831 100644 --- a/Tactical/Turn Based Input.cpp +++ b/Tactical/Turn Based Input.cpp @@ -1700,6 +1700,24 @@ void ItemCreationCallBack( UINT8 ubResult ) memset(gszMsgBoxInputString,0,sizeof(gszMsgBoxInputString)); } +static void CycleThroughTileDebugInfo() +{ + const STR16 modeStrings[] = + { + L"Pathfinding", + L"Threat values", + L"Cover values", + L"Off", + }; + + gRenderDebugInfoMode += 1; + if (gRenderDebugInfoMode > DEBUG_OFF) + { + gRenderDebugInfoMode = 0; + } + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, modeStrings[gRenderDebugInfoMode]); +} + extern BOOLEAN gfDisableRegionActive; extern BOOLEAN gfUserTurnRegionActive; @@ -4763,6 +4781,13 @@ void GetKeyboardInput( UINT32 *puiNewEvent ) break; case 'Z': + if (fCtrl) + { + if (DEBUG_CHEAT_LEVEL()) + { + CycleThroughTileDebugInfo(); + } + } break; } diff --git a/TacticalAI/FindLocations.cpp b/TacticalAI/FindLocations.cpp index 33faab039..02f1f5ab0 100644 --- a/TacticalAI/FindLocations.cpp +++ b/TacticalAI/FindLocations.cpp @@ -15,10 +15,7 @@ #include "Render Fun.h" #include "Boxing.h" #include "Text.h" - #ifdef _DEBUG - #include "renderworld.h" - #include "video.h" - #endif + #include "renderworld.h" #include "worldman.h" #include "strategicmap.h" #include "environment.h" @@ -27,6 +24,7 @@ #include "GameSettings.h" #include "Soldier Profile.h" #include "rotting corpses.h" // sevenfm +#include //////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -34,17 +32,7 @@ // were changed to GetAPsCrouch() and GetAPsProne() // - also all "APBPConstants[AP_PICKUP_ITEM]" were replaced by GetBasicAPsToPickupItem() //////////////////////////////////////////////////////////////////////////////////////////////////////// - - INT16 * gsCoverValue = NULL; -#ifdef _DEBUG - - INT16 gsBestCover; - #ifndef PATHAI_VISIBLE_DEBUG - // NB Change this to true to get visible cover debug -- CJC - BOOLEAN gfDisplayCoverValues = FALSE; - #endif - extern void RenderCoverDebug( void ); -#endif +INT16 gsBestCover; INT16 gubAIPathCosts[19][19]; @@ -693,6 +681,11 @@ static void CalculateCoverValue(SOLDIERTYPE* pSoldier, const INT32 sGridNo, cons INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentBetter, INT32 targetGridNo) { + if (gRenderDebugInfoMode == DEBUG_COVERVALUE && DEBUG_CHEAT_LEVEL()) + { + ResetDebugInfoValues(); + } + DebugMsg(TOPIC_JA2AI,DBG_LEVEL_3,String("FindBestNearbyCover")); // all 32-bit integers for max. speed @@ -761,12 +754,6 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB iBestCoverValue = -1; -#if defined( _DEBUG ) && defined( COVER_DEBUG ) - if (gfDisplayCoverValues) - { - memset( gsCoverValue, 0x7F, sizeof( INT16 ) * WORLD_MAX ); - } -#endif //NameMessage(pSoldier,"looking for some cover..."); @@ -850,6 +837,11 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // cover we are searching for must be better than what we have now! CalculateCoverValue(pSoldier, targetGridNo, 0, iMyThreatValue, uiThreatCnt, ubDiff, fNight, ubBackgroundLightPercent, morale, iCurrentCoverValue, iCurrentScale); + if (gRenderDebugInfoMode == DEBUG_COVERVALUE && DEBUG_CHEAT_LEVEL()) + { + gRenderDebugInfoValues[targetGridNo] = (INT32)(iCurrentCoverValue / 100); + } + #ifdef DEBUGCOVER // AINumMessage("Search Range = ",iSearchRange); #endif @@ -1057,12 +1049,10 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB } #endif -#if defined( _DEBUG ) && defined( COVER_DEBUG ) - if (gfDisplayCoverValues) + if (gRenderDebugInfoMode == DEBUG_COVERVALUE && DEBUG_CHEAT_LEVEL()) { - gsCoverValue[sGridNo] = (INT16) (iCoverValue / 100); + gRenderDebugInfoValues[sGridNo] = (INT32) (iCoverValue / 100); } -#endif // if this is better than the best place found so far @@ -1099,34 +1089,15 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB gubNPCAPBudget = 0; gubNPCDistLimit = 0; - #if defined( _DEBUG ) && !defined( PATHAI_VISIBLE_DEBUG ) - if (gfDisplayCoverValues) + if (gRenderDebugInfoMode == DEBUG_COVERVALUE && DEBUG_CHEAT_LEVEL()) { - // do a locate? - LocateSoldier( pSoldier->ubID, SETLOCATORFAST ); gsBestCover = sBestCover; - SetRenderFlags( RENDER_FLAG_FULL ); - RenderWorld(); - RenderCoverDebug( ); - InvalidateScreen( ); - EndFrameBufferRender(); - RefreshScreen( NULL ); - /* - iLoop = GetJA2Clock(); - do - { - - } while( ( GetJA2Clock( ) - iLoop ) < 2000 ); - */ + InvalidateRegion(gsVIEWPORT_START_X, gsVIEWPORT_START_Y, gsVIEWPORT_END_X, gsVIEWPORT_WINDOW_END_Y); } - #endif // if a better cover location was found if (!TileIsOutOfBounds(sBestCover)) { - #if defined( _DEBUG ) && !defined( PATHAI_VISIBLE_DEBUG ) - gsBestCover = sBestCover; - #endif // cover values already take the AP cost of getting there into account in // a BIG way, so no need to worry about that here, even small improvements // are actually very significant once we get our APs back (if we live!) diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index 60f0f45c9..d2a32b5b8 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -12,10 +12,6 @@ extern INT16 gubAIPathCosts[19][19]; #define AI_PATHCOST_RADIUS 9 -extern BOOLEAN gfDisplayCoverValues; -//extern INT16 gsCoverValue[WORLD_MAX]; -extern INT16 * gsCoverValue; - // AI actions enum CreatureCalls diff --git a/TileEngine/renderworld.cpp b/TileEngine/renderworld.cpp index 85eb7bd9d..5e03e74b5 100644 --- a/TileEngine/renderworld.cpp +++ b/TileEngine/renderworld.cpp @@ -23,6 +23,7 @@ #include "LogicalBodyTypes/BodyTypeDB.h" #include "Utilities.h" +#include UINT32 guiShieldGraphic = 0; BOOLEAN fShieldGraphicInit = FALSE; @@ -552,22 +553,25 @@ void ResetRenderParameters( ); void RenderRoomInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPointX_S, INT16 sStartPointY_S, INT16 sEndXS, INT16 sEndYS ); - -#ifdef _DEBUG -//extern UINT8 gubFOVDebugInfoInfo[ WORLD_MAX ]; -//extern UINT8 gubGridNoMarkers[ WORLD_MAX ]; -extern UINT8 * gubFOVDebugInfoInfo; -extern UINT8 * gubGridNoMarkers; -extern UINT8 gubGridNoValue; -extern BOOLEAN gfDisplayCoverValues; -extern BOOLEAN gfDisplayGridNoVisibleValues = 0; -//extern INT16 gsCoverValue[ WORLD_MAX ]; -extern INT16 * gsCoverValue; -extern INT16 gsBestCover; -void RenderFOVDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPointX_S, INT16 sStartPointY_S, INT16 sEndXS, INT16 sEndYS ); -void RenderCoverDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPointX_S, INT16 sStartPointY_S, INT16 sEndXS, INT16 sEndYS ); -void RenderGridNoVisibleDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPointX_S, INT16 sStartPointY_S, INT16 sEndXS, INT16 sEndYS ); -#endif +//extern UINT8 * gubFOVDebugInfoInfo; +//extern UINT8 * gubGridNoMarkers; +//extern UINT8 gubGridNoValue; +//extern BOOLEAN gfDisplayGridNoVisibleValues = 0; +//void RenderFOVDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPointX_S, INT16 sStartPointY_S, INT16 sEndXS, INT16 sEndYS ); +//void RenderGridNoVisibleDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPointX_S, INT16 sStartPointY_S, INT16 sEndXS, INT16 sEndYS ); +extern INT16 gsBestCover; +INT32* gRenderDebugInfoValues = nullptr; +void RenderDebugInfo(INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPointX_S, INT16 sStartPointY_S, INT16 sEndXS, INT16 sEndYS); +void ResetDebugInfoValues() +{ + if (gRenderDebugInfoValues) + { + for (size_t i = 0; i < WORLD_MAX; i++) + { + gRenderDebugInfoValues[i] = 0x7FFFFFFF; + } + } +} void DeleteFromWorld( UINT16 usTileIndex, UINT32 uiRenderTiles, UINT16 usIndex ); @@ -3363,24 +3367,11 @@ UINT32 cnt = 0; RenderRoomInfo( gsStartPointX_M, gsStartPointY_M, gsStartPointX_S, gsStartPointY_S, gsEndXS, gsEndYS ); } -#ifdef _DEBUG - if( gRenderFlags&RENDER_FLAG_FOVDEBUG ) - { - RenderFOVDebugInfo( gsStartPointX_M, gsStartPointY_M, gsStartPointX_S, gsStartPointY_S, gsEndXS, gsEndYS ); - } - else if (gfDisplayCoverValues) - { - RenderCoverDebugInfo( gsStartPointX_M, gsStartPointY_M, gsStartPointX_S, gsStartPointY_S, gsEndXS, gsEndYS ); - } - else if (gfDisplayGridNoVisibleValues) + if (DEBUG_CHEAT_LEVEL() && gTacticalStatus.Team[OUR_TEAM].bTeamActive) { - RenderGridNoVisibleDebugInfo( gsStartPointX_M, gsStartPointY_M, gsStartPointX_S, gsStartPointY_S, gsEndXS, gsEndYS ); + RenderDebugInfo(gsStartPointX_M, gsStartPointY_M, gsStartPointX_S, gsStartPointY_S, gsEndXS, gsEndYS); } -#endif - -//#endif - //RenderStaticWorldRect( gsVIEWPORT_START_X, gsVIEWPORT_START_Y, gsVIEWPORT_END_X, gsVIEWPORT_END_Y ); //AddBaseDirtyRect(gsVIEWPORT_START_X, gsVIEWPORT_START_Y, gsVIEWPORT_END_X, gsVIEWPORT_END_Y ); @@ -8105,7 +8096,6 @@ void RenderRoomInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPoi #ifdef _DEBUG - void RenderFOVDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPointX_S, INT16 sStartPointY_S, INT16 sEndXS, INT16 sEndYS ) { INT8 bXOddFlag = 0; @@ -8215,7 +8205,8 @@ void RenderFOVDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStar } -void RenderCoverDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPointX_S, INT16 sStartPointY_S, INT16 sEndXS, INT16 sEndYS ) +// This one just renders tile gridnos. Not much use nowadays +void RenderGridNoVisibleDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPointX_S, INT16 sStartPointY_S, INT16 sEndXS, INT16 sEndYS ) { INT8 bXOddFlag = 0; INT16 sAnchorPosX_M, sAnchorPosY_M; @@ -8223,8 +8214,8 @@ void RenderCoverDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sSt INT16 sTempPosX_M, sTempPosY_M; INT16 sTempPosX_S, sTempPosY_S; BOOLEAN fEndRenderRow = FALSE, fEndRenderCol = FALSE; - UINT16 usTileIndex; INT16 sX, sY; + INT32 usTileIndex;//dnl ch56 141009 UINT32 uiDestPitchBYTES; UINT8 *pDestBuf; @@ -8264,25 +8255,19 @@ void RenderCoverDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sSt sY -= gpWorldLevelData[ usTileIndex ].sHeight; sY += gsRenderHeight; - if (gsCoverValue[ usTileIndex] != 0x7F7F) + SetFont( SMALLCOMPFONT ); + SetFontDestBuffer( FRAME_BUFFER , 0, 0, SCREEN_WIDTH, gsVIEWPORT_END_Y, FALSE ); + + if ( !GridNoOnVisibleWorldTile( usTileIndex ) ) { - SetFont( SMALLCOMPFONT ); - SetFontDestBuffer( FRAME_BUFFER , 0, 0, SCREEN_WIDTH, gsVIEWPORT_END_Y, FALSE ); - if (usTileIndex == gsBestCover) - { - SetFontForeground( FONT_MCOLOR_RED ); - } - else if (gsCoverValue[ usTileIndex ] < 0) - { - SetFontForeground( FONT_MCOLOR_WHITE ); - } - else - { - SetFontForeground( FONT_GRAY3 ); - } - mprintf_buffer( pDestBuf, uiDestPitchBYTES, TINYFONT1, sX, sY , L"%d", gsCoverValue[ usTileIndex ] ); - SetFontDestBuffer( FRAME_BUFFER , 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, FALSE ); + SetFontForeground( FONT_MCOLOR_RED ); } + else + { + SetFontForeground( FONT_GRAY3 ); + } + mprintf_buffer( pDestBuf, uiDestPitchBYTES, TINYFONT1, sX, sY , L"%d", usTileIndex ); + SetFontDestBuffer( FRAME_BUFFER , 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, FALSE ); } @@ -8321,112 +8306,138 @@ void RenderCoverDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sSt UnLockVideoSurface( FRAME_BUFFER ); } +#endif -void RenderGridNoVisibleDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPointX_S, INT16 sStartPointY_S, INT16 sEndXS, INT16 sEndYS ) +void RenderDebugInfo(INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPointX_S, INT16 sStartPointY_S, INT16 sEndXS, INT16 sEndYS) { - INT8 bXOddFlag = 0; - INT16 sAnchorPosX_M, sAnchorPosY_M; - INT16 sAnchorPosX_S, sAnchorPosY_S; - INT16 sTempPosX_M, sTempPosY_M; - INT16 sTempPosX_S, sTempPosY_S; - BOOLEAN fEndRenderRow = FALSE, fEndRenderCol = FALSE; - INT16 sX, sY; - INT32 usTileIndex;//dnl ch56 141009 - UINT32 uiDestPitchBYTES; - UINT8 *pDestBuf; - + INT8 bXOddFlag = 0; + INT16 sTempPosX_M, sTempPosY_M; + INT16 sTempPosX_S, sTempPosY_S; + BOOLEAN fEndRenderRow = FALSE, fEndRenderCol = FALSE; // Begin Render Loop - sAnchorPosX_M = sStartPointX_M; - sAnchorPosY_M = sStartPointY_M; - sAnchorPosX_S = sStartPointX_S; - sAnchorPosY_S = sStartPointY_S; + INT16 sAnchorPosX_M = sStartPointX_M; + INT16 sAnchorPosY_M = sStartPointY_M; + INT16 sAnchorPosX_S = sStartPointX_S; + INT16 sAnchorPosY_S = sStartPointY_S; - pDestBuf = LockVideoSurface( FRAME_BUFFER, &uiDestPitchBYTES ); + UINT32 uiDestPitchBYTES; + UINT8* pDestBuf = LockVideoSurface(FRAME_BUFFER, &uiDestPitchBYTES); + + const auto mode = gRenderDebugInfoMode; do { - fEndRenderRow = FALSE; sTempPosX_M = sAnchorPosX_M; sTempPosY_M = sAnchorPosY_M; sTempPosX_S = sAnchorPosX_S; sTempPosY_S = sAnchorPosY_S; - if(bXOddFlag > 0) + if (bXOddFlag > 0) sTempPosX_S += 20; - do { - - usTileIndex=FASTMAPROWCOLTOPOS( sTempPosY_M, sTempPosX_M ); - - if ( usTileIndex < GRIDSIZE ) + UINT16 usTileIndex = FASTMAPROWCOLTOPOS(sTempPosY_M, sTempPosX_M); + if (usTileIndex < GRIDSIZE) { - sX = sTempPosX_S + ( WORLD_TILE_X / 2 ) - 5; - sY = sTempPosY_S + ( WORLD_TILE_Y / 2 ) - 5; + INT16 sX = sTempPosX_S + (WORLD_TILE_X / 2) - 5; + INT16 sY = sTempPosY_S + (WORLD_TILE_Y / 2) - 5; // Adjust for interface level - sY -= gpWorldLevelData[ usTileIndex ].sHeight; + sY -= gpWorldLevelData[usTileIndex].sHeight; sY += gsRenderHeight; - SetFont( SMALLCOMPFONT ); - SetFontDestBuffer( FRAME_BUFFER , 0, 0, SCREEN_WIDTH, gsVIEWPORT_END_Y, FALSE ); - if ( !GridNoOnVisibleWorldTile( usTileIndex ) ) + if (gRenderDebugInfoValues[usTileIndex] != 0x7FFFFFFF) { - SetFontForeground( FONT_MCOLOR_RED ); - } - else - { - SetFontForeground( FONT_GRAY3 ); + SetFont(SMALLCOMPFONT); + SetFontDestBuffer(FRAME_BUFFER, 0, 0, SCREEN_WIDTH, gsVIEWPORT_END_Y, FALSE); + + + //////////////////////////// + // Debug mode specific setup + switch (mode) + { + case DEBUG_PATHFINDING: + if (gRenderDebugInfoValues[usTileIndex] < 0) + { + SetFontForeground(FONT_LTRED); + } + else + { + SetFontForeground(FONT_LTGREEN); + } + break; + case DEBUG_THREATVALUE: + //TODO implement + goto exit_loop; + break; + case DEBUG_COVERVALUE: + if (usTileIndex == gsBestCover) + { + SetFontForeground(FONT_YELLOW); + } + else if (gRenderDebugInfoValues[usTileIndex] < 0) + { + SetFontForeground(FONT_LTRED); + } + else + { + SetFontForeground(FONT_LTGREEN); + } + break; + + default: + goto exit_loop; + break; + } + //////////////////////////// + + + mprintf_buffer(pDestBuf, uiDestPitchBYTES, TINYFONT1, sX, sY, L"%d", gRenderDebugInfoValues[usTileIndex]); + SetFontDestBuffer(FRAME_BUFFER, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, FALSE); } - mprintf_buffer( pDestBuf, uiDestPitchBYTES, TINYFONT1, sX, sY , L"%d", usTileIndex ); - SetFontDestBuffer( FRAME_BUFFER , 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, FALSE ); } sTempPosX_S += 40; - sTempPosX_M ++; - sTempPosY_M --; + sTempPosX_M++; + sTempPosY_M--; - if ( sTempPosX_S >= sEndXS ) + if (sTempPosX_S >= sEndXS) { fEndRenderRow = TRUE; } - } while( !fEndRenderRow ); + } while (!fEndRenderRow); - if ( bXOddFlag > 0 ) + if (bXOddFlag > 0) { - sAnchorPosY_M ++; + sAnchorPosY_M++; } else { - sAnchorPosX_M ++; + sAnchorPosX_M++; } bXOddFlag = !bXOddFlag; sAnchorPosY_S += 10; - if ( sAnchorPosY_S >= sEndYS ) + if (sAnchorPosY_S >= sEndYS) { fEndRenderCol = TRUE; } + } while (!fEndRenderCol); - } - while( !fEndRenderCol ); - - UnLockVideoSurface( FRAME_BUFFER ); - + exit_loop: + UnLockVideoSurface(FRAME_BUFFER); } -#endif - void ExamineZBufferRect( INT16 sLeft, INT16 sTop, INT16 sRight, INT16 sBottom) { @@ -9088,21 +9099,3 @@ void SetRenderCenter( INT16 sNewX, INT16 sNewY ) gfScrollInertia = FALSE; } - -#ifdef _DEBUG -void RenderFOVDebug( ) -{ - RenderFOVDebugInfo( gsStartPointX_M, gsStartPointY_M, gsStartPointX_S, gsStartPointY_S, gsEndXS, gsEndYS ); -} - -void RenderCoverDebug( ) -{ - RenderCoverDebugInfo( gsStartPointX_M, gsStartPointY_M, gsStartPointX_S, gsStartPointY_S, gsEndXS, gsEndYS ); -} - -void RenderGridNoVisibleDebug( ) -{ - RenderGridNoVisibleDebugInfo( gsStartPointX_M, gsStartPointY_M, gsStartPointX_S, gsStartPointY_S, gsEndXS, gsEndYS ); -} - -#endif diff --git a/TileEngine/renderworld.h b/TileEngine/renderworld.h index 140e5d5f4..4ba08f78e 100644 --- a/TileEngine/renderworld.h +++ b/TileEngine/renderworld.h @@ -216,6 +216,16 @@ void SetRenderCenter( INT16 sNewX, INT16 sNewY ); #ifdef _DEBUG void RenderFOVDebug( ); #endif +enum RenderDebugInfoModes +{ + DEBUG_PATHFINDING, + DEBUG_THREATVALUE, + DEBUG_COVERVALUE, + DEBUG_OFF +}; +void ResetDebugInfoValues(); +extern INT32* gRenderDebugInfoValues; + BOOLEAN Zero8BPPDataTo16BPPBufferTransparent( UINT16 *pBuffer, UINT32 uiDestPitchBYTES, HVOBJECT hSrcVObject, INT32 iX, INT32 iY, UINT16 usIndex ); BOOLEAN Blt8BPPDataTo16BPPBufferTransZIncClipZSameZBurnsThrough( UINT16 *pBuffer, UINT32 uiDestPitchBYTES, UINT16 *pZBuffer, UINT16 usZValue, HVOBJECT hSrcVObject, INT32 iX, INT32 iY, UINT16 usIndex, SGPRect *clipregion, INT16 sZStripIndex ); @@ -227,4 +237,4 @@ BOOLEAN Blt8BPPDataTo16BPPBufferTransZIncObscureClip(UINT16 *pBuffer, UINT32 uiD BOOLEAN Blt8BPPDataTo16BPPBufferTransZTransShadowIncObscureClip(UINT16 *pBuffer, UINT32 uiDestPitchBYTES, UINT16 *pZBuffer, UINT16 usZValue, HVOBJECT hSrcVObject, INT32 iX, INT32 iY, UINT16 usIndex, SGPRect *clipregion, INT16 sZIndex, UINT16 *p16BPPPalette, BOOLEAN fIgnoreShadows = FALSE); BOOLEAN Blt8BPPDataTo16BPPBufferTransZTransShadowIncObscureClipAlpha(UINT16 *pBuffer, UINT32 uiDestPitchBYTES, UINT16 *pZBuffer, UINT16 usZValue, HVOBJECT hSrcVObject, HVOBJECT hAlphaVObject, INT32 iX, INT32 iY, UINT16 usIndex, SGPRect *clipregion, INT16 sZIndex, UINT16 *p16BPPPalette, BOOLEAN fIgnoreShadows = FALSE); -#endif \ No newline at end of file +#endif diff --git a/TileEngine/worlddef.cpp b/TileEngine/worlddef.cpp index 628a35e3d..012568a3a 100644 --- a/TileEngine/worlddef.cpp +++ b/TileEngine/worlddef.cpp @@ -49,6 +49,7 @@ #include "Button Defines.h" #include "Animation Data.h" #endif +#include #define SET_MOVEMENTCOST( a, b, c, d ) ( ( gubWorldMovementCosts[ a ][ b ][ c ] < d ) ? ( gubWorldMovementCosts[ a ][ b ][ c ] = d ) : 0 ); @@ -84,7 +85,6 @@ extern UINT8 *gubFOVDebugInfoInfo; extern INT16 gsFullTileDirections[MAX_FULLTILE_DIRECTIONS]; extern INT32 dirDelta[8]; extern INT16 DirIncrementer[8]; -extern INT16 *gsCoverValue; extern INT32 gsTempActionGridNo; extern INT32 gsOverItemsGridNo; extern INT32 gsOutOfRangeGridNo; @@ -320,8 +320,6 @@ void DeinitializeWorld() TrashWorld(); if(gubGridNoMarkers) MemFree(gubGridNoMarkers); - if(gsCoverValue) - MemFree(gsCoverValue); if(gubBuildingInfo) MemFree(gubBuildingInfo); if(gusWorldRoomInfo) @@ -4313,10 +4311,12 @@ void SetWorldSize(INT32 nWorldRows, INT32 nWorldCols) gubGridNoMarkers = (UINT8*)MemAlloc(WORLD_MAX); memset(gubGridNoMarkers, 0, sizeof(UINT8)*WORLD_MAX); - if(gsCoverValue) - MemFree(gsCoverValue); - gsCoverValue = (INT16*)MemAlloc(sizeof(INT16)*WORLD_MAX); - memset(gsCoverValue, 0x7F, sizeof(INT16)*WORLD_MAX); + if (DEBUG_CHEAT_LEVEL()) + { + MemFree(gRenderDebugInfoValues); + gRenderDebugInfoValues = (INT32*)MemAlloc(sizeof(INT32) * WORLD_MAX); + ResetDebugInfoValues(); + } // Init building structures and variables if(gubBuildingInfo) From 2e3e4b38d9ac4d09795d8a9f670053fac3dce1fa Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 25 Dec 2025 20:17:02 +0200 Subject: [PATCH 49/80] Fix compilation --- Tactical/Boxing.cpp | 1 + Tactical/Handle UI.cpp | 2 +- Tactical/Handle UI.h | 1 + Tactical/PATHAI.cpp | 3 +++ TacticalAI/AIMain.cpp | 9 --------- TileEngine/renderworld.cpp | 16 ++++++++++++++++ 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/Tactical/Boxing.cpp b/Tactical/Boxing.cpp index dacef732e..d6be71e39 100644 --- a/Tactical/Boxing.cpp +++ b/Tactical/Boxing.cpp @@ -21,6 +21,7 @@ #include "GameSettings.h" // added by SANDRO #include #include +#include "Soldier macros.h" INT32 gsBoxerGridNo[ NUM_BOXERS ] = { 11393, 11233, 11073 }; SoldierID gubBoxerID[ NUM_BOXERS ] = { NOBODY, NOBODY, NOBODY }; diff --git a/Tactical/Handle UI.cpp b/Tactical/Handle UI.cpp index 20c2321ac..3f6daf80f 100644 --- a/Tactical/Handle UI.cpp +++ b/Tactical/Handle UI.cpp @@ -519,7 +519,7 @@ void HandleRenderDebugInfoModes() static INT32 previousLocation = NOWHERE; static UINT8 previousStance = 0; - UINT16 usSoldierIndex = NOBODY; + SoldierID usSoldierIndex = NOBODY; UINT32 uiMercFlags; FindSoldierFromMouse(&usSoldierIndex, &uiMercFlags); if (usSoldierIndex == NOBODY) diff --git a/Tactical/Handle UI.h b/Tactical/Handle UI.h index acfc4cab4..ae12e4130 100644 --- a/Tactical/Handle UI.h +++ b/Tactical/Handle UI.h @@ -184,6 +184,7 @@ extern UINT32 guiCurrentEvent; extern INT16 gsSelectedLevel; extern BOOLEAN gfPlotNewMovement; extern UINT32 guiPendingOverrideEvent; +extern UINT8 gRenderDebugInfoMode; // GLOBALS diff --git a/Tactical/PATHAI.cpp b/Tactical/PATHAI.cpp index b0bee6991..404bdd714 100644 --- a/Tactical/PATHAI.cpp +++ b/Tactical/PATHAI.cpp @@ -36,6 +36,9 @@ #include "BinaryHeap.hpp" #include "opplist.h" #include "Weapons.h" +#include "renderworld.h" +#include "Cheats.h" +#include "Handle UI.h" //forward declarations of common classes to eliminate includes class OBJECTTYPE; diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index e2a417d41..beb3a108b 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -463,13 +463,6 @@ BOOLEAN InitAI( void ) FILE * DebugFile; #endif -#ifdef _DEBUG - if (gfDisplayCoverValues) - { - //memset( gsCoverValue, 0x7F, sizeof( INT16 ) * WORLD_MAX ); - } -#endif - //If we are not loading a saved game ( if we are, this has already been called ) if( !( gTacticalStatus.uiFlags & LOADING_SAVED_GAME ) ) { @@ -849,10 +842,8 @@ void HandleSoldierAI( SOLDIERTYPE *pSoldier ) // FIXME - this function is named #ifdef JA2TESTVERSION // display deadlock message gfUIInDeadlock = TRUE; - gUIDeadlockedSoldier = pSoldier->ubID; DebugAI( String("DEADLOCK soldier %d action %s ABC %d", pSoldier->ubID.i, gzActionStr[pSoldier->aiData.bAction], gTacticalStatus.ubAttackBusyCount ) ); #else - // If we are in beta version, also report message! #ifdef JA2BETAVERSION ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_ERROR, L"Aborting AI deadlock for %d. Please sent DEBUG.TXT file and SAVE.", pSoldier->ubID.i ); diff --git a/TileEngine/renderworld.cpp b/TileEngine/renderworld.cpp index ad63ae944..7aed36546 100644 --- a/TileEngine/renderworld.cpp +++ b/TileEngine/renderworld.cpp @@ -8005,6 +8005,11 @@ void RenderRoomInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPoi #ifdef _DEBUG +// From fov.cpp +extern UINT8* gubGridNoMarkers; +extern UINT8 gubGridNoValue; +extern UINT8* gubFOVDebugInfoInfo; + void RenderFOVDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, INT16 sStartPointX_S, INT16 sStartPointY_S, INT16 sEndXS, INT16 sEndYS ) { INT8 bXOddFlag = 0; @@ -8215,6 +8220,17 @@ void RenderGridNoVisibleDebugInfo( INT16 sStartPointX_M, INT16 sStartPointY_M, I UnLockVideoSurface( FRAME_BUFFER ); } + + +void RenderFOVDebug() +{ + RenderFOVDebugInfo(gsStartPointX_M, gsStartPointY_M, gsStartPointX_S, gsStartPointY_S, gsEndXS, gsEndYS); +} + +void RenderGridNoVisibleDebug() +{ + RenderGridNoVisibleDebugInfo(gsStartPointX_M, gsStartPointY_M, gsStartPointX_S, gsStartPointY_S, gsEndXS, gsEndYS); +} #endif From 4e19816b57e65497fbc260a7b1a52a7ddb347cf2 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 00:28:19 +0200 Subject: [PATCH 50/80] Fix typo --- Tactical/Soldier Control.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tactical/Soldier Control.cpp b/Tactical/Soldier Control.cpp index b82bce830..778f6ae13 100644 --- a/Tactical/Soldier Control.cpp +++ b/Tactical/Soldier Control.cpp @@ -19046,7 +19046,7 @@ BOOLEAN SOLDIERTYPE::IsJamming( ) { if ( CanUseRadio( FALSE ) ) return TRUE; - // if we cannot use the radio, remove that flag hile we're at it + // if we cannot use the radio, remove that flag while we're at it else usSoldierFlagMask &= ~SOLDIER_RADIO_OPERATOR_JAMMING; } From f5b705cbe4dec9148325c58e8ec9dc89d39c6fef Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:05:10 +0200 Subject: [PATCH 51/80] Add ingame option to use old AI instead of new --- Ja2/GameSettings.cpp | 3 +++ Ja2/GameSettings.h | 1 + i18n/_ChineseText.cpp | 2 ++ i18n/_DutchText.cpp | 2 ++ i18n/_EnglishText.cpp | 2 ++ i18n/_FrenchText.cpp | 2 ++ i18n/_GermanText.cpp | 2 ++ i18n/_ItalianText.cpp | 2 ++ i18n/_PolishText.cpp | 2 ++ i18n/_RussianText.cpp | 2 ++ 10 files changed, 20 insertions(+) diff --git a/Ja2/GameSettings.cpp b/Ja2/GameSettings.cpp index eaa0f8c2c..059c3d76e 100644 --- a/Ja2/GameSettings.cpp +++ b/Ja2/GameSettings.cpp @@ -332,6 +332,7 @@ BOOLEAN LoadGameSettings() gGameSettings.fOptions[TOPTION_ALT_START_AIM] = iniReader.ReadBoolean("JA2 Game Settings", "TOPTION_ALT_START_AIM" , TRUE); // Start at max aiming level instead of default no aiming gGameSettings.fOptions[TOPTION_ALT_PATHFINDING] = iniReader.ReadBoolean("JA2 Game Settings", "TOPTION_ALT_PATHFINDING" , FALSE); // A* pathfinding + gGameSettings.fOptions[TOPTION_USE_LEGACY_TACTICALAI] = iniReader.ReadBoolean("JA2 Game Settings", "TOPTION_USE_LEGACY_TACTICALAI", FALSE); // A* pathfinding gGameSettings.fOptions[TOPTION_MERCENARY_FORMATIONS] = iniReader.ReadBoolean("JA2 Game Settings","TOPTION_MERCENARY_FORMATIONS" , FALSE ); // Flugente: mercenary formations gGameSettings.fOptions[TOPTION_SHOW_ENEMY_LOCATION] = iniReader.ReadBoolean("JA2 Game Settings","TOPTION_SHOW_ENEMY_LOCATION" , FALSE); // sevenfm: show locations of known enemies gGameSettings.fOptions[TOPTION_REPORT_MISS_MARGIN] = iniReader.ReadBoolean("JA2 Game Settings","TOPTION_REPORT_MISS_MARGIN" , FALSE ); // HEADROCK HAM 4: Shot offset report @@ -616,6 +617,7 @@ BOOLEAN SaveGameSettings() settings << "TOPTION_SHOW_ENEMY_LOCATION = " << (gGameSettings.fOptions[TOPTION_SHOW_ENEMY_LOCATION] ? "TRUE" : "FALSE" ) << endl; settings << "TOPTION_ALT_START_AIM = " << (gGameSettings.fOptions[TOPTION_ALT_START_AIM] ? "TRUE" : "FALSE") << endl; settings << "TOPTION_ALT_PATHFINDING = " << (gGameSettings.fOptions[TOPTION_ALT_PATHFINDING] ? "TRUE" : "FALSE") << endl; + settings << "TOPTION_USE_LEGACY_TACTICALAI = " << (gGameSettings.fOptions[TOPTION_USE_LEGACY_TACTICALAI] ? "TRUE" : "FALSE") << endl; settings << "TOPTION_CHEAT_MODE_OPTIONS_HEADER = " << (gGameSettings.fOptions[TOPTION_CHEAT_MODE_OPTIONS_HEADER] ? "TRUE" : "FALSE" ) << endl; settings << "TOPTION_FORCE_BOBBY_RAY_SHIPMENTS = " << (gGameSettings.fOptions[TOPTION_FORCE_BOBBY_RAY_SHIPMENTS] ? "TRUE" : "FALSE" ) << endl; @@ -846,6 +848,7 @@ void InitGameSettings() gGameSettings.fOptions[TOPTION_SHOW_ENEMY_LOCATION] = FALSE; // sevenfm: show locations of known enemies gGameSettings.fOptions[TOPTION_ALT_START_AIM] = TRUE; gGameSettings.fOptions[TOPTION_ALT_PATHFINDING] = FALSE; + gGameSettings.fOptions[TOPTION_USE_LEGACY_TACTICALAI] = FALSE; // arynn: Cheat/Debug Menu gGameSettings.fOptions[ TOPTION_CHEAT_MODE_OPTIONS_HEADER ] = FALSE; diff --git a/Ja2/GameSettings.h b/Ja2/GameSettings.h index 540276603..4f2a5ffbc 100644 --- a/Ja2/GameSettings.h +++ b/Ja2/GameSettings.h @@ -105,6 +105,7 @@ enum TOPTION_SHOW_ENEMY_LOCATION, TOPTION_ALT_START_AIM, TOPTION_ALT_PATHFINDING, + TOPTION_USE_LEGACY_TACTICALAI, // arynn: Debug/Cheat TOPTION_CHEAT_MODE_OPTIONS_HEADER, diff --git a/i18n/_ChineseText.cpp b/i18n/_ChineseText.cpp index 2f951f6d3..9572f89e9 100644 --- a/i18n/_ChineseText.cpp +++ b/i18n/_ChineseText.cpp @@ -6641,6 +6641,7 @@ STR16 zOptionsToggleText[] = L"显示已知敌人位置", //L"Show enemy location", show locator on last known enemy location L"准心开始时为最大", // L"Start at maximum aim", L"替换新的寻路方式", // L"Alternative pathfinding", + L"Use old Tactical AI", // L"Use old Tactical AI", L"--作弊模式选项--", // TOPTION_CHEAT_MODE_OPTIONS_HEADER, L"强制 Bobby Ray 送货", // force all pending Bobby Ray shipments L"-----------------", // TOPTION_CHEAT_MODE_OPTIONS_END @@ -6762,6 +6763,7 @@ STR16 zOptionsScreenHelpText[] = L"打开时,会显示已知敌人最后移动的位置。", //L"When ON, shows last known enemy location.", L"打开时,默认瞄准值为最大,而不是无。", //L"When ON, aiming at enemy will start at maximum aiming instead of default no aim", L"打开时,使用A*寻路算法,而不是原始算法。", //L"When ON, Use A* pathfinding algorithm, instead of original", + L"When ON, Uses the old tactical AI", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_HEADER", L"强制 Bobby Ray 出货", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_END", diff --git a/i18n/_DutchText.cpp b/i18n/_DutchText.cpp index 509040f7d..c846c607a 100644 --- a/i18n/_DutchText.cpp +++ b/i18n/_DutchText.cpp @@ -6643,6 +6643,7 @@ STR16 zOptionsToggleText[] = L"Show enemy location", // show locator on last known enemy location L"Start at maximum aim", L"Alternative pathfinding", + L"Use old Tactical AI", L"--Cheat Mode Options--", // TOPTION_CHEAT_MODE_OPTIONS_HEADER, L"Force Bobby Ray shipments", // force all pending Bobby Ray shipments L"-----------------", // TOPTION_CHEAT_MODE_OPTIONS_END @@ -6764,6 +6765,7 @@ STR16 zOptionsScreenHelpText[] = L"When ON, shows last known enemy location.", //TODO.Translate L"When ON, aiming at enemy will start at maximum aiming instead of default no aim", L"When ON, Use A* pathfinding algorithm, instead of original", + L"When ON, Uses the old tactical AI", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_HEADER", L"Force all pending Bobby Ray shipments", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_END", diff --git a/i18n/_EnglishText.cpp b/i18n/_EnglishText.cpp index f3284709f..ba31cd982 100644 --- a/i18n/_EnglishText.cpp +++ b/i18n/_EnglishText.cpp @@ -6641,6 +6641,7 @@ STR16 zOptionsToggleText[] = L"Show enemy location", // show locator on last known enemy location L"Start at maximum aim", L"Alternative pathfinding", + L"Use old Tactical AI", L"--Cheat Mode Options--", // TOPTION_CHEAT_MODE_OPTIONS_HEADER, L"Force Bobby Ray Shipments", // force all pending Bobby Ray shipments L"-----------------", // TOPTION_CHEAT_MODE_OPTIONS_END @@ -6762,6 +6763,7 @@ STR16 zOptionsScreenHelpText[] = L"When ON, shows last known enemy location.", L"When ON, aiming at enemy will start at maximum aiming instead of default no aim", L"When ON, Use A* pathfinding algorithm, instead of original", + L"When ON, Uses the old tactical AI", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_HEADER", L"Force all pending Bobby Ray shipments", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_END", diff --git a/i18n/_FrenchText.cpp b/i18n/_FrenchText.cpp index a859908d8..955c72292 100644 --- a/i18n/_FrenchText.cpp +++ b/i18n/_FrenchText.cpp @@ -6648,6 +6648,7 @@ STR16 zOptionsToggleText[] = L"Show enemy location", // show locator on last known enemy location L"Start at maximum aim", L"Alternative pathfinding", + L"Use old Tactical AI", L"--Options mode triche--", // TOPTION_CHEAT_MODE_OPTIONS_HEADER, L"Forcer envois Bobby Ray", // force all pending Bobby Ray shipments L"-----------------", // TOPTION_CHEAT_MODE_OPTIONS_END @@ -6768,6 +6769,7 @@ STR16 zOptionsScreenHelpText[] = L"When ON, shows last known enemy location.", //TODO.Translate L"When ON, aiming at enemy will start at maximum aiming instead of default no aim", L"When ON, Use A* pathfinding algorithm, instead of original", + L"When ON, Uses the old tactical AI", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_HEADER", L"Forcer tous les envois en attente de Bobby Ray", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_END", diff --git a/i18n/_GermanText.cpp b/i18n/_GermanText.cpp index cfd6a7ed7..cbfc96828 100644 --- a/i18n/_GermanText.cpp +++ b/i18n/_GermanText.cpp @@ -6515,6 +6515,7 @@ STR16 zOptionsToggleText[] = L"Show enemy location", // show locator on last known enemy location L"Start at maximum aim", L"Alternative pathfinding", + L"Use old Tactical AI", L"--Cheat Mode Options--", // TOPTION_CHEAT_MODE_OPTIONS_HEADER, L"Erzwinge BR Lieferung", // force all pending Bobby Ray shipments L"-----------------", // TOPTION_CHEAT_MODE_OPTIONS_END @@ -6636,6 +6637,7 @@ STR16 zOptionsScreenHelpText[] = L"When ON, shows last known enemy location.", //TODO.Translate L"When ON, aiming at enemy will start at maximum aiming instead of default no aim", L"When ON, Use A* pathfinding algorithm, instead of original", + L"When ON, Uses the old tactical AI", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_HEADER", L"Force all pending Bobby Ray shipments", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_END", diff --git a/i18n/_ItalianText.cpp b/i18n/_ItalianText.cpp index a8da7833b..b610a9d93 100644 --- a/i18n/_ItalianText.cpp +++ b/i18n/_ItalianText.cpp @@ -6626,6 +6626,7 @@ STR16 zOptionsToggleText[] = L"Show enemy location", // show locator on last known enemy location L"Start at maximum aim", L"Alternative pathfinding", + L"Use old Tactical AI", L"--Cheat Mode Options--", // TOPTION_CHEAT_MODE_OPTIONS_HEADER, L"Force Bobby Ray shipments", // force all pending Bobby Ray shipments L"-----------------", // TOPTION_CHEAT_MODE_OPTIONS_END @@ -6747,6 +6748,7 @@ STR16 zOptionsScreenHelpText[] = L"When ON, shows last known enemy location.", //TODO.Translate L"When ON, aiming at enemy will start at maximum aiming instead of default no aim", L"When ON, Use A* pathfinding algorithm, instead of original", + L"When ON, Uses the old tactical AI", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_HEADER", L"Force all pending Bobby Ray shipments", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_END", diff --git a/i18n/_PolishText.cpp b/i18n/_PolishText.cpp index 526206375..31d201878 100644 --- a/i18n/_PolishText.cpp +++ b/i18n/_PolishText.cpp @@ -6645,6 +6645,7 @@ STR16 zOptionsToggleText[] = L"Show enemy location", // show locator on last known enemy location L"Start at maximum aim", L"Alternative pathfinding", + L"Use old Tactical AI", L"--Cheat Mode Options--", // TOPTION_CHEAT_MODE_OPTIONS_HEADER, L"Force Bobby Ray shipments", // force all pending Bobby Ray shipments L"-----------------", // TOPTION_CHEAT_MODE_OPTIONS_END @@ -6766,6 +6767,7 @@ STR16 zOptionsScreenHelpText[] = L"When ON, shows last known enemy location.", //TODO.Translate L"When ON, aiming at enemy will start at maximum aiming instead of default no aim", L"When ON, Use A* pathfinding algorithm, instead of original", + L"When ON, Uses the old tactical AI", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_HEADER", L"Wymuś wszystkie oczekiwane dostawy od Bobby Ray's.", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_END", diff --git a/i18n/_RussianText.cpp b/i18n/_RussianText.cpp index ae91c3121..334ec1d36 100644 --- a/i18n/_RussianText.cpp +++ b/i18n/_RussianText.cpp @@ -6637,6 +6637,7 @@ STR16 zOptionsToggleText[] = L"Показывать расположение", // show locator on last known enemy location L"Start at maximum aim", L"Alternative pathfinding", + L"Use old Tactical AI", L"--Читерские настройки--", // TOPTION_CHEAT_MODE_OPTIONS_HEADER, L"Ускорить доставку Бобби Рэя", // force all pending Bobby Ray shipments L"-----------------", // TOPTION_CHEAT_MODE_OPTIONS_END @@ -6758,6 +6759,7 @@ STR16 zOptionsScreenHelpText[] = L"Если включено, показывает известное расположение противника\n(нажмите |S|h|i|f|t, чтобы показать источник шума).", L"When ON, aiming at enemy will start at maximum aiming instead of default no aim", L"When ON, Use A* pathfinding algorithm, instead of original", + L"When ON, Uses the old tactical AI", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_HEADER", L"Выберите этот пункт,\nчтобы груз Бобби Рэя прибыл немедленно.", L"(text not rendered)TOPTION_CHEAT_MODE_OPTIONS_END", From e634edd862c85b001fe7bca240569c2944c901d8 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:05:31 +0200 Subject: [PATCH 52/80] Rename debug draw modes --- Tactical/Turn Based Input.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tactical/Turn Based Input.cpp b/Tactical/Turn Based Input.cpp index 96f1920c3..c7a0b3227 100644 --- a/Tactical/Turn Based Input.cpp +++ b/Tactical/Turn Based Input.cpp @@ -1688,10 +1688,10 @@ static void CycleThroughTileDebugInfo() { const STR16 modeStrings[] = { - L"Pathfinding", - L"Threat values", - L"Cover values", - L"Off", + L"Debug draw mode: Pathfinding", + L"Debug drawmode: Threat values", + L"Debug drawmode: Cover values", + L"Debug drawmode: Off", }; gRenderDebugInfoMode += 1; From 16fc2d8177278495e26a7d1cf11dec3a6d762560 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:29:06 +0200 Subject: [PATCH 53/80] Cleanup FindBestNearbyCover * Add optional parameter to ignore search range --- Tactical/Handle UI.cpp | 2 +- TacticalAI/FindLocations.cpp | 99 +++++++----------------------------- TacticalAI/ai.h | 2 +- 3 files changed, 20 insertions(+), 83 deletions(-) diff --git a/Tactical/Handle UI.cpp b/Tactical/Handle UI.cpp index 3f6daf80f..7d1420aef 100644 --- a/Tactical/Handle UI.cpp +++ b/Tactical/Handle UI.cpp @@ -535,7 +535,7 @@ void HandleRenderDebugInfoModes() GetSoldier(&pSoldier, usSoldierIndex); if (previousSoldier != pSoldier || previousLocation != pSoldier->sGridNo || previousStance != gAnimControl[pSoldier->usAnimState].ubEndHeight) { - FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iPercentBetter); + FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iPercentBetter, NOWHERE, false); previousSoldier = pSoldier; previousLocation = pSoldier->sGridNo; previousStance = gAnimControl[pSoldier->usAnimState].ubEndHeight; diff --git a/TacticalAI/FindLocations.cpp b/TacticalAI/FindLocations.cpp index 923105cf4..967b6efa2 100644 --- a/TacticalAI/FindLocations.cpp +++ b/TacticalAI/FindLocations.cpp @@ -680,47 +680,31 @@ static void CalculateCoverValue(SOLDIERTYPE* pSoldier, const INT32 sGridNo, cons } } -INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentBetter, INT32 targetGridNo) +INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentBetter, INT32 targetGridNo, bool ignoreSearchRange) { if (gRenderDebugInfoMode == DEBUG_COVERVALUE && DEBUG_CHEAT_LEVEL()) { ResetDebugInfoValues(); } - DebugMsg(TOPIC_JA2AI,DBG_LEVEL_3,String("FindBestNearbyCover")); // all 32-bit integers for max. speed - UINT32 uiLoop; INT32 iCurrentCoverValue, iCoverValue, iBestCoverValue; INT32 iCurrentScale = -1, iCoverScale = -1, iBestCoverScale = -1; INT32 iDistFromOrigin, iDistCoverFromOrigin; - //INT32 iThreatCertainty; INT32 sGridNo, sBestCover = NOWHERE; INT32 iPathCost; - INT32 iThreatRange, iClosestThreatRange = 1500; -// INT16 sClosestThreatGridno = NOWHERE; INT32 iMyThreatValue; - //INT32 sThreatLoc; - //INT32 iMaxThreatRange; UINT32 uiThreatCnt = 0; INT32 iMaxMoveTilesLeft, iSearchRange, iRoamRange; INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXOffset, sYOffset; INT32 sOrigin; // has to be a short, need a pointer - INT32 * pusLastLoc; - INT8 * pbPersOL; - INT8 * pbPublOL; - //SOLDIERTYPE *pOpponent; UINT16 usMovementMode; - UINT8 ubBackgroundLightLevel; UINT8 ubBackgroundLightPercent = 0; - UINT8 ubLightPercentDifference; BOOLEAN fNight; - // sevenfm - UINT8 ubNearbyFriends; - BOOLEAN fProneCover; - UINT8 ubDiff = SoldierDifficultyLevel( pSoldier ); + const UINT8 ubDiff = SoldierDifficultyLevel( pSoldier ); if (targetGridNo == NOWHERE) { @@ -753,19 +737,6 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB } } - iBestCoverValue = -1; - - - //NameMessage(pSoldier,"looking for some cover..."); - - // BUILD A LIST OF THREATENING GRID #s FROM PERSONAL & PUBLIC opplists - - pusLastLoc = &(gsLastKnownOppLoc[pSoldier->ubID][0]); - - // hang a pointer into personal opplist - pbPersOL = &(pSoldier->aiData.bOppList[0]); - // hang a pointer into public opplist - pbPublOL = &(gbPublicOpplist[pSoldier->bTeam][0]); // decide how far we're gonna be looking iSearchRange = gbDiff[DIFF_MAX_COVER_RANGE][ SoldierDifficultyLevel( pSoldier ) ]; @@ -795,6 +766,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB usMovementMode = DetermineMovementMode( pSoldier, AI_ACTION_TAKE_COVER ); +#if 1 if (pSoldier->aiData.bAlertStatus >= STATUS_RED) // if already in battle { // must be able to reach the cover, so it can't possibly be more than @@ -815,6 +787,12 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB iSearchRange = iMaxMoveTilesLeft; } } +#endif + if (ignoreSearchRange) + { + // For debugging + iSearchRange = 16; + } if (iSearchRange <= 0) { @@ -837,32 +815,25 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // calculate our current cover value in the place we are now, since the // cover we are searching for must be better than what we have now! CalculateCoverValue(pSoldier, targetGridNo, 0, iMyThreatValue, uiThreatCnt, ubDiff, fNight, ubBackgroundLightPercent, morale, iCurrentCoverValue, iCurrentScale); + // the initial cover value to beat is our current cover value + iBestCoverValue = iCurrentCoverValue; if (gRenderDebugInfoMode == DEBUG_COVERVALUE && DEBUG_CHEAT_LEVEL()) { gRenderDebugInfoValues[targetGridNo] = (INT32)(iCurrentCoverValue / 100); } -#ifdef DEBUGCOVER -// AINumMessage("Search Range = ",iSearchRange); -#endif - // determine maximum horizontal limits sMaxLeft = min(iSearchRange,(targetGridNo % MAXCOL)); - //NumMessage("sMaxLeft = ",sMaxLeft); sMaxRight = min(iSearchRange,MAXCOL - ((targetGridNo % MAXCOL) + 1)); - //NumMessage("sMaxRight = ",sMaxRight); - // determine maximum vertical limits sMaxUp = min(iSearchRange,(targetGridNo / MAXROW)); - //NumMessage("sMaxUp = ",sMaxUp); sMaxDown = min(iSearchRange,MAXROW - ((targetGridNo / MAXROW) + 1)); - //NumMessage("sMaxDown = ",sMaxDown); iRoamRange = RoamingRange(pSoldier,&sOrigin); - // if status isn't black (life & death combat), and roaming range is limited - if ((pSoldier->aiData.bAlertStatus != STATUS_BLACK) && (iRoamRange < MAX_ROAMING_RANGE) && + //if ((pSoldier->aiData.bAlertStatus != STATUS_BLACK) && (iRoamRange < MAX_ROAMING_RANGE) && + if ((pSoldier->aiData.bAlertStatus < STATUS_RED) && (iRoamRange < MAX_ROAMING_RANGE) && (!TileIsOutOfBounds(sOrigin))) { // must try to stay within or return to the point of origin @@ -875,19 +846,6 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB } -#ifdef DEBUGCOVER - DebugAI( String( "FBNC: iRoamRange %d, sMaxLeft %d, sMaxRight %d, sMaxUp %d, sMaxDown %d\n",iRoamRange,sMaxLeft,sMaxRight,sMaxUp,sMaxDown) ); -#endif - - // the initial cover value to beat is our current cover value - iBestCoverValue = iCurrentCoverValue; - -#ifdef DEBUGDECISIONS - STR tempstr=""; - sprintf( tempstr, "FBNC: CURRENT iCoverValue = %d\n",iCurrentCoverValue ); - DebugAI( tempstr ); -#endif - if (pSoldier->aiData.bAlertStatus >= STATUS_RED) // if already in battle { // to speed this up, tell PathAI to cancel any paths beyond our AP reach! @@ -921,7 +879,6 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB gpWorldLevelData[sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE); } } - FindBestPath( pSoldier, GRIDSIZE, pSoldier->pathing.bLevel, DetermineMovementMode( pSoldier, AI_ACTION_TAKE_COVER ), COPYREACHABLE_AND_APS, 0 );//dnl ch50 071009 // Turn off the "reachable" flag for his current location @@ -933,7 +890,6 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB { for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++) { - //HandleMyMouseCursor(KEYBOARDALSO); // calculate the next potential gridno sGridNo = targetGridNo + sXOffset + (MAXCOL * sYOffset); @@ -1042,13 +998,6 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // EVALUATE EACH GRID #, remembering the BEST PROTECTED ONE CalculateCoverValue(pSoldier, sGridNo, iPathCost, iMyThreatValue, uiThreatCnt, ubDiff, fNight, ubBackgroundLightPercent, morale, iCoverValue, iCoverScale); -#ifdef DEBUGCOVER - // if there ARE multiple opponents - if (uiThreatCnt > 1) - { - DebugAI( String( "FBNC: Total iCoverValue at gridno %d is %d\n\n",sGridNo,iCoverValue ) ); - } -#endif if (gRenderDebugInfoMode == DEBUG_COVERVALUE && DEBUG_CHEAT_LEVEL()) { @@ -1056,7 +1005,6 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB } // if this is better than the best place found so far - if (iCoverValue > iBestCoverValue) { // ONLY DO THIS CHECK HERE IF WE'RE WAITING FOR OPPCHANCETODECIDE, @@ -1074,11 +1022,6 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB } */ -#ifdef DEBUGDECISIONS - STR tempstr; - sprintf( tempstr,"FBNC: NEW BEST iCoverValue at gridno %d is %d\n",sGridNo,iCoverValue ); - DebugAI( tempstr ); -#endif // remember it instead sBestCover = sGridNo; iBestCoverValue = iCoverValue; @@ -1107,16 +1050,6 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB // if best cover value found was at least 5% better than our current cover if (*piPercentBetter >= MIN_PERCENT_BETTER) { -#ifdef DEBUGDECISIONS - STR tempstr; - sprintf( tempstr,"Found Cover: current %ld, best %ld, %%%%Better %ld\n", iCurrentCoverValue,iBestCoverValue,*piPercentBetter ); - DebugAI( tempstr ); -#endif - -#ifdef BETAVERSION - SnuggleDebug(pSoldier,"Found Cover"); -#endif - return(sBestCover); // return the gridno of that cover } } @@ -3035,7 +2968,11 @@ INT32 FindAdvanceSpot(SOLDIERTYPE *pSoldier, INT32 sTargetSpot, INT8 bAction, UI { if (usMovementMode == RUNNING || usMovementMode == WALKING) { - ubReserveAP = APBPConstants[AP_CHANGE_FACING] + GetAPsCrouch(pSoldier, TRUE) + GetAPsProne(pSoldier, TRUE); + ubReserveAP = APBPConstants[AP_CHANGE_FACING] + GetAPsCrouch(pSoldier, TRUE) +GetAPsProne(pSoldier, TRUE); + if (pSoldier->bActionPoints <= ubReserveAP) + { + ubReserveAP = APBPConstants[AP_CHANGE_FACING] + GetAPsCrouch(pSoldier, TRUE); + } } else { diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index a120a7534..9fd8a10e7 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -195,7 +195,7 @@ void EndAIGuysTurn( SOLDIERTYPE *pSoldier ); INT8 ExecuteAction(SOLDIERTYPE *pSoldier); INT32 FindAdjacentSpotBeside(SOLDIERTYPE *pSoldier, INT32 sGridNo); -INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *pPercentBetter, INT32 targetGridNo = NOWHERE); +INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *pPercentBetter, INT32 targetGridNo = NOWHERE, bool ignoreSearchRange = false); INT32 FindClosestDoor( SOLDIERTYPE * pSoldier ); INT32 FindNearbyPointOnEdgeOfMap( SOLDIERTYPE * pSoldier, INT8 * pbDirection ); INT32 FindNearestEdgePoint( INT32 sGridNo ); From a46d2dbb96234dafc7a2e57fe4ae22dce49df73c Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:31:34 +0200 Subject: [PATCH 54/80] Add global to selectively log AI status BLACK decisions --- TacticalAI/AIMain.cpp | 3 ++- TacticalAI/DecideAction.cpp | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index beb3a108b..312bbae51 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -286,7 +286,8 @@ STR16 wszAction[] = { UINT32 guiAIStartCounter = 0, guiAILastCounter = 0; //UINT8 gubAISelectedSoldier = NOBODY; BOOLEAN gfLogsEnabled = TRUE; -bool gLogDecideActionRed = false; +bool gLogDecideActionRed = true; +bool gLogDecideActionBlack = true; void DebugAI( INT8 bMsgType, SOLDIERTYPE *pSoldier, STR szOutput, bool doLog, INT8 bAction) { diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 5f24d2b88..bc6aee600 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -41,6 +41,7 @@ ////////////////////////////////////////////////////////////////////// extern bool gLogDecideActionRed; +extern bool gLogDecideActionBlack; extern BOOLEAN gfHiddenInterrupt; extern BOOLEAN gfUseAlternateQueenPosition; extern UINT16 PickSoldierReadyAnimation( SOLDIERTYPE *pSoldier, BOOLEAN fEndReady, BOOLEAN fHipStance ); From c3f4ee754a983f19f6d172664973b99ef0fceb29 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:32:01 +0200 Subject: [PATCH 55/80] Only log during turn based AI --- TacticalAI/AIMain.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 312bbae51..cb4f8b78f 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -295,8 +295,7 @@ void DebugAI( INT8 bMsgType, SOLDIERTYPE *pSoldier, STR szOutput, bool doLog, IN CHAR8 msg[1024]; CHAR8 buf[1024]; - - if (!gfLogsEnabled || !doLog || pSoldier == nullptr) + if (!gfTurnBasedAI || !gfLogsEnabled || !doLog || pSoldier == nullptr) return; memset(buf, 0, 1024 * sizeof(char)); From cd054b549ff23766f3058ac9adffec27c37b0400 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:50:27 +0200 Subject: [PATCH 56/80] Split DecideAction into several specialized DecideAction functions * Removes the need to constantly check for fCivilian, ARMED_VEHICLE, ENEMY_ROBOT etc inside DecideAction * Move AI decision checks into their own functions, such as DecideActionRadioRedAlert * Add new entry to ActionType. AI_ACTION_INVALID signals that no valid action was found when a function returns, or no valid action is possible * Do a weighted random selection for choosing target under DecideActionBlackSoldier --- ModularizedTacticalAI/CMakeLists.txt | 4 + ModularizedTacticalAI/include/BoxerPlan.h | 23 + ModularizedTacticalAI/include/CivilianPlan.h | 23 + ModularizedTacticalAI/include/RobotPlan.h | 23 + ModularizedTacticalAI/include/SoldierPlan.h | 23 + ModularizedTacticalAI/src/BoxerPlan.cpp | 35 + ModularizedTacticalAI/src/CivilianPlan.cpp | 35 + .../src/LegacyAIPlanFactory.cpp | 12 +- ModularizedTacticalAI/src/RobotPlan.cpp | 34 + ModularizedTacticalAI/src/SoldierPlan.cpp | 104 + TacticalAI/AIInternals.h | 4 +- TacticalAI/AIMain.cpp | 2 +- TacticalAI/Attacks.cpp | 684 + TacticalAI/DecideAction.cpp | 11691 +++++++++++++++- TacticalAI/PanicButtons.cpp | 8 +- TacticalAI/ai.h | 25 +- 16 files changed, 12707 insertions(+), 23 deletions(-) create mode 100644 ModularizedTacticalAI/include/BoxerPlan.h create mode 100644 ModularizedTacticalAI/include/CivilianPlan.h create mode 100644 ModularizedTacticalAI/include/RobotPlan.h create mode 100644 ModularizedTacticalAI/include/SoldierPlan.h create mode 100644 ModularizedTacticalAI/src/BoxerPlan.cpp create mode 100644 ModularizedTacticalAI/src/CivilianPlan.cpp create mode 100644 ModularizedTacticalAI/src/RobotPlan.cpp create mode 100644 ModularizedTacticalAI/src/SoldierPlan.cpp diff --git a/ModularizedTacticalAI/CMakeLists.txt b/ModularizedTacticalAI/CMakeLists.txt index 0c4c78ba9..a9e800007 100644 --- a/ModularizedTacticalAI/CMakeLists.txt +++ b/ModularizedTacticalAI/CMakeLists.txt @@ -6,6 +6,10 @@ set(ModularizedTacticalAISrc "${CMAKE_CURRENT_SOURCE_DIR}/src/LegacyAIPlanFactory.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/LegacyCreaturePlan.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/LegacyZombiePlan.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/src/BoxerPlan.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/src/CivilianPlan.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/src/RobotPlan.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/src/SoldierPlan.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/NullPlan.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/NullPlanFactory.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/Plan.cpp" diff --git a/ModularizedTacticalAI/include/BoxerPlan.h b/ModularizedTacticalAI/include/BoxerPlan.h new file mode 100644 index 000000000..0c3075900 --- /dev/null +++ b/ModularizedTacticalAI/include/BoxerPlan.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Plan.h" + +namespace AI +{ + namespace tactical + { + /**@class LegacyAIPlan + * @brief Component/Concrete Product. Wrapper/Re-Write of DecideAction() + * + * Wrapper around boxer related AI uplifted from original DecideAction() routines + */ + class LegacyAIBoxerPlan: public Plan + { + private: + public: + LegacyAIBoxerPlan(SOLDIERTYPE* npc); + virtual void execute(PlanInputData& environment); + virtual bool done() const {return false;} + }; + } +} diff --git a/ModularizedTacticalAI/include/CivilianPlan.h b/ModularizedTacticalAI/include/CivilianPlan.h new file mode 100644 index 000000000..4998c550a --- /dev/null +++ b/ModularizedTacticalAI/include/CivilianPlan.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Plan.h" + +namespace AI +{ + namespace tactical + { + /**@class LegacyAIPlan + * @brief Component/Concrete Product. Wrapper/Re-Write of DecideAction() + * + * Wrapper around civilian/noncombatant related AI uplifted from original DecideAction() routines + */ + class CivilianPlan : public Plan + { + private: + public: + CivilianPlan(SOLDIERTYPE* npc); + virtual void execute(PlanInputData& environment); + virtual bool done() const { return false; } + }; + } +} diff --git a/ModularizedTacticalAI/include/RobotPlan.h b/ModularizedTacticalAI/include/RobotPlan.h new file mode 100644 index 000000000..e4c173d78 --- /dev/null +++ b/ModularizedTacticalAI/include/RobotPlan.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Plan.h" + +namespace AI +{ + namespace tactical + { + /**@class LegacyAIPlan + * @brief Component/Concrete Product. Wrapper/Re-Write of DecideAction() + * + * Wrapper around robot related AI uplifted from original DecideAction() routines + */ + class RobotPlan : public Plan + { + private: + public: + RobotPlan(SOLDIERTYPE* npc); + virtual void execute(PlanInputData& environment); + virtual bool done() const { return false; } + }; + } +} diff --git a/ModularizedTacticalAI/include/SoldierPlan.h b/ModularizedTacticalAI/include/SoldierPlan.h new file mode 100644 index 000000000..46309ea92 --- /dev/null +++ b/ModularizedTacticalAI/include/SoldierPlan.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Plan.h" + +namespace AI +{ + namespace tactical + { + /**@class LegacyAIPlan + * @brief Component/Concrete Product. Wrapper/Re-Write of DecideAction() + * + * Wrapper around soldier related AI uplifted from original DecideAction() routines + */ + class SoldierPlan : public Plan + { + private: + public: + SoldierPlan(SOLDIERTYPE* npc); + virtual void execute(PlanInputData& environment); + virtual bool done() const { return false; } + }; + } +} diff --git a/ModularizedTacticalAI/src/BoxerPlan.cpp b/ModularizedTacticalAI/src/BoxerPlan.cpp new file mode 100644 index 000000000..8f5a0dfa0 --- /dev/null +++ b/ModularizedTacticalAI/src/BoxerPlan.cpp @@ -0,0 +1,35 @@ +#include "../include/BoxerPlan.h" +#include "../../TacticalAI/ai.h" + +namespace AI +{ + namespace tactical + { + LegacyAIBoxerPlan::LegacyAIBoxerPlan(SOLDIERTYPE* npc) + : Plan(npc) + { + } + + + void LegacyAIBoxerPlan::execute(PlanInputData& environment) + { + switch (get_npc()->aiData.bAlertStatus) + { + case STATUS_GREEN: + get_npc()->aiData.bAction = DecideActionGreenBoxer(get_npc()); + break; + case STATUS_YELLOW: + get_npc()->aiData.bAction = DecideActionGreenBoxer(get_npc()); + break; + case STATUS_RED: + get_npc()->aiData.bAction = DecideActionBlackBoxer(get_npc()); + break; + case STATUS_BLACK: + get_npc()->aiData.bAction = DecideActionBlackBoxer(get_npc()); + break; + } + } + + } // namespace tactical +} // namespace AI + diff --git a/ModularizedTacticalAI/src/CivilianPlan.cpp b/ModularizedTacticalAI/src/CivilianPlan.cpp new file mode 100644 index 000000000..abd302bcc --- /dev/null +++ b/ModularizedTacticalAI/src/CivilianPlan.cpp @@ -0,0 +1,35 @@ +#include "../include/CivilianPlan.h" +#include "../../TacticalAI/ai.h" + +namespace AI +{ + namespace tactical + { + CivilianPlan::CivilianPlan(SOLDIERTYPE* npc) + : Plan(npc) + { + } + + + void CivilianPlan::execute(PlanInputData& environment) + { + switch (get_npc()->aiData.bAlertStatus) + { + case STATUS_GREEN: + get_npc()->aiData.bAction = DecideActionGreenCivilian(get_npc()); + break; + case STATUS_YELLOW: + get_npc()->aiData.bAction = DecideActionYellowCivilian(get_npc()); + break; + case STATUS_RED: + get_npc()->aiData.bAction = DecideActionRedCivilian(get_npc()); + break; + case STATUS_BLACK: + get_npc()->aiData.bAction = DecideActionBlackCivilian(get_npc()); + break; + } + } + + } // namespace tactical +} // namespace AI + diff --git a/ModularizedTacticalAI/src/LegacyAIPlanFactory.cpp b/ModularizedTacticalAI/src/LegacyAIPlanFactory.cpp index e8c29a831..96c8f52c9 100644 --- a/ModularizedTacticalAI/src/LegacyAIPlanFactory.cpp +++ b/ModularizedTacticalAI/src/LegacyAIPlanFactory.cpp @@ -7,6 +7,10 @@ #include "../include/LegacyCreaturePlan.h" #include "../include/LegacyZombiePlan.h" #include "../include/LegacyArmedVehiclePlan.h" +#include "../include/BoxerPlan.h" +#include "../include/CivilianPlan.h" +#include "../include/RobotPlan.h" +#include "../include/SoldierPlan.h" #include "../include/CrowPlan.h" #include "../include/PlanList.h" @@ -14,8 +18,7 @@ #include "../../Tactical/Soldier Control.h" // For SOLDIERTYPE definition #include "../../Tactical/Animation Data.h" // For the definition of, wait for it... BLOODCAT! #include "Soldier macros.h" - -#include +#include "ai.h" namespace AI @@ -43,6 +46,11 @@ namespace AI if(npc->IsZombie()) return new LegacyZombiePlan(npc); + if (BOXER(npc)) { return new LegacyAIBoxerPlan(npc); } + if (IS_CIV_BODY_TYPE(npc)) { return new CivilianPlan(npc); } + if (ENEMYROBOT(npc)) { return new RobotPlan(npc); } + if (SoldierAI(npc)) { return new SoldierPlan(npc); } + return new LegacyAIPlan(npc); // no special plan for other cases yet, return default legacy AI wrapper } diff --git a/ModularizedTacticalAI/src/RobotPlan.cpp b/ModularizedTacticalAI/src/RobotPlan.cpp new file mode 100644 index 000000000..9e82bd380 --- /dev/null +++ b/ModularizedTacticalAI/src/RobotPlan.cpp @@ -0,0 +1,34 @@ +#include "../include/RobotPlan.h" +#include "../../TacticalAI/ai.h" + +namespace AI +{ + namespace tactical + { + RobotPlan::RobotPlan(SOLDIERTYPE* npc) + : Plan(npc) + { + } + + + void RobotPlan::execute(PlanInputData& environment) + { + switch (get_npc()->aiData.bAlertStatus) + { + case STATUS_GREEN: + get_npc()->aiData.bAction = DecideActionGreenRobot(get_npc()); + break; + case STATUS_YELLOW: + get_npc()->aiData.bAction = DecideActionYellowRobot(get_npc()); + break; + case STATUS_RED: + get_npc()->aiData.bAction = DecideActionRedRobot(get_npc()); + break; + case STATUS_BLACK: + get_npc()->aiData.bAction = DecideActionBlackRobot(get_npc()); + break; + } + } + + } // namespace tactical +} // namespace AI diff --git a/ModularizedTacticalAI/src/SoldierPlan.cpp b/ModularizedTacticalAI/src/SoldierPlan.cpp new file mode 100644 index 000000000..76018b098 --- /dev/null +++ b/ModularizedTacticalAI/src/SoldierPlan.cpp @@ -0,0 +1,104 @@ +#include "../include/SoldierPlan.h" +#include "../../TacticalAI/ai.h" +#include "../../TacticalAI/AIInternals.h" // ACTING_ON_SCHEDULE +#include "../../TacticalAI/NPC.h" // NPCReachedDestination +#include "../../Tactical/Dialogue Control.h" // DialogueQueueIsEmpty +#include "../../Utils/Font Control.h" // ScreenMsg about deadlock +#include "../../i18n/include/Text.h" // Sniper warning +#include "../../Utils/message.h" // ditto + +namespace AI +{ + namespace tactical + { + SoldierPlan::SoldierPlan(SOLDIERTYPE* npc) + : Plan(npc) + { + } + + + void SoldierPlan::execute(PlanInputData& environment) + { + if (!environment.turn_based()) + { + if ((get_npc()->ubProfile != NO_PROFILE) && (gMercProfiles[get_npc()->ubProfile].ubMiscFlags3 & PROFILE_MISC_FLAG3_HANDLE_DONE_TRAVERSAL)) + { + TriggerNPCWithGivenApproach(get_npc()->ubProfile, APPROACH_DONE_TRAVERSAL, FALSE); + gMercProfiles[get_npc()->ubProfile].ubMiscFlags3 &= (~PROFILE_MISC_FLAG3_HANDLE_DONE_TRAVERSAL); + get_npc()->ubQuoteActionID = 0; + // wait a tiny bit + get_npc()->aiData.usActionData = 100; + get_npc()->aiData.bAction = AI_ACTION_WAIT; + return; + } + if (get_npc()->bTeam == gbPlayerNum) + { + if (environment.get_tactical_status().fAutoBandageMode) + { + get_npc()->aiData.bAction = DecideAutoBandage(get_npc()); + return; + } + } + } + + if (get_npc()->bTeam != MILITIA_TEAM) + { + if (!sniperwarning && get_npc()->aiData.bOrders == SNIPER) + { + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, New113Message[MSG113_WATHCHOUTFORSNIPERS]); + sniperwarning = TRUE; + + // Flugente: additional dialogue + AdditionalTacticalCharacterDialogue_AllInCurrentSector(NO_PROFILE, ADE_SNIPERWARNING); + } + + if (!biggunwarning && FindRocketLauncherOrCannon(get_npc()) != NO_SLOT) + { + biggunwarning = TRUE; + //TODO: don't say this again after reloading a savegame + SayQuoteFromAnyBodyInSector(QUOTE_WEARY_SLASH_SUSPUCIOUS); + } + } + get_npc()->aiData.fAIFlags &= (~AI_CAUTIOUS); // turn off cautious flag + // if status override is set, bypass RED/YELLOW and go directly to GREEN! + if ((get_npc()->aiData.bBypassToGreen) && (get_npc()->aiData.bAlertStatus < STATUS_BLACK)) + { + get_npc()->aiData.bAction = DecideActionGreenSoldier(get_npc()); + if (!gfTurnBasedAI) + { + // reset bypass now + get_npc()->aiData.bBypassToGreen = 0; + } + } + else + { + switch (get_npc()->aiData.bAlertStatus) + { + case STATUS_GREEN: + get_npc()->aiData.bAction = DecideActionGreenSoldier(get_npc()); + break; + case STATUS_YELLOW: + get_npc()->aiData.bAction = DecideActionYellowSoldier(get_npc()); + break; + case STATUS_RED: + get_npc()->aiData.bAction = DecideActionRedSoldier(get_npc()); + break; + case STATUS_BLACK: + //if ( gGameSettings.fOptions[TOPTION_USE_LEGACY_TACTICALAI] ) // Commented out for now since new AI is WIP + { + get_npc()->aiData.bAction = DecideActionBlackSoldier(get_npc()); + } + //else + //{ + // get_npc()->aiData.bAction = DecideActionBlackSoldierUtilityAI(get_npc()); + //} + break; + } + } + DEBUGAIMSG("Deciding for guynum " << (int)get_npc()->ubID << " at gridno " << get_npc()->sGridNo << ", APs " << get_npc()->bActionPoints << + ", decided action: " << (int)get_npc()->aiData.bAction << ", data " << (int)get_npc()->aiData.usActionData); + } + + } // namespace tactical +} // namespace AI + diff --git a/TacticalAI/AIInternals.h b/TacticalAI/AIInternals.h index 37172e45c..2e6d35ead 100644 --- a/TacticalAI/AIInternals.h +++ b/TacticalAI/AIInternals.h @@ -4,7 +4,7 @@ #include "Overhead.h" #include "random.h" #include "Points.h" - +#include "ai.h" #include #include @@ -239,7 +239,7 @@ INT32 GetInterveningClimbingLocation( SOLDIERTYPE * pSoldier, INT32 sDestGridNo, UINT8 GetTraversalQuoteActionID( INT8 bDirection ); INT32 GoAsFarAsPossibleTowards(SOLDIERTYPE *pSoldier, INT32 sDesGrid, INT8 bAction); -INT8 HeadForTheStairCase( SOLDIERTYPE * pSoldier ); +ActionType HeadForTheStairCase( SOLDIERTYPE * pSoldier ); BOOLEAN InSmoke(INT32 sGridNo, INT8 bLevel); BOOLEAN InGas( SOLDIERTYPE *pSoldier, INT32 sGridNo ); diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index cb4f8b78f..8786656ee 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -336,7 +336,7 @@ void DebugAI( INT8 bMsgType, SOLDIERTYPE *pSoldier, STR szOutput, bool doLog, IN strcat(msg, buf); } - if (bAction >= AI_ACTION_NONE && bAction <= AI_ACTION_LAST) + if (bAction >= AI_ACTION_NONE && bAction < AI_ACTION_INVALID) { strcat(msg, " "); strcat(msg, szAction[bAction]); diff --git a/TacticalAI/Attacks.cpp b/TacticalAI/Attacks.cpp index bc33eb3cf..94439da83 100644 --- a/TacticalAI/Attacks.cpp +++ b/TacticalAI/Attacks.cpp @@ -4234,3 +4234,687 @@ void CheckTossGrenadeAt(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestThrow, INT32 sTa pSoldier->bWeaponMode = WM_NORMAL; } + + +static void CalculatePossibleShots(SOLDIERTYPE* pSoldier, INT8 gun, std::vector &possibleShots) +{ + UINT32 uiLoop; + INT32 iAttackValue, iThreatValue, iHitRate, iBestHitRate, iPercentBetter, iEstDamage, iTrueLastTarget; + UINT16 usTrueState, usTurningCost, usRaiseGunCost; + INT16 sMinAPcost; + INT16 sRawAPCost; + INT16 sAimAPCost; + INT16 sBestAPcost; + INT16 sChanceToHit; + INT16 sAimTime; + INT16 sBestAimTime; + INT16 sMaxPossibleAimTime; + UINT8 ubChanceToGetThrough; + UINT8 ubBestChanceToGetThrough; + UINT8 ubFriendlyFireChance; + UINT8 ubBestFriendlyFireChance; + INT16 sBestChanceToHit; + INT16 sStanceAPcost; + BOOLEAN fAddingTurningCost, fAddingRaiseGunCost; + UINT8 ubStance, ubBestStance, ubChanceToReallyHit; + INT8 bScopeMode; + SOLDIERTYPE* pOpponent; + + // sevenfm: + BOOLEAN fSuppression = FALSE; + BOOLEAN fReturnFire = FALSE; + INT32 sTarget = NOWHERE; + INT8 bLevel; + + INT8 bKnowledge; + INT8 bPersonalKnowledge; + INT8 bPublicKnowledge; + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "CalcBestShot"); + + + sBestChanceToHit = sBestAimTime = sChanceToHit = ubBestChanceToGetThrough = ubBestFriendlyFireChance = ubChanceToReallyHit = 0; + + // sevenfm: set attacking hand and target + pSoldier->ubAttackingHand = HANDPOS; + pSoldier->ubTargetID = NOBODY; + + pSoldier->usAttackingWeapon = pSoldier->inv[HANDPOS].usItem; + pSoldier->bWeaponMode = WM_NORMAL; + + std::map ObjList; + GetScopeLists(pSoldier, &pSoldier->inv[HANDPOS], ObjList); + pSoldier->bScopeMode = USE_BEST_SCOPE; + pSoldier->bDoBurst = 0; + pSoldier->bDoAutofire = 0; + + // determine which attack against which target has the greatest attack value + for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) + { + ATTACKTYPE pBestShot; + pBestShot.ubPossible = FALSE; + pBestShot.ubChanceToReallyHit = 0; + pBestShot.iAttackValue = 0; + pBestShot.ubOpponent = NOBODY; + pBestShot.ubFriendlyFireChance = 0; + pBestShot.bWeaponIn = gun; + + pOpponent = MercSlots[uiLoop]; + fSuppression = FALSE; + fReturnFire = FALSE; + + // if this merc is inactive, at base, on assignment, or dead + if (!pOpponent || !pOpponent->stats.bLife) + continue; // next merc + + if (!ValidOpponent(pSoldier, pOpponent)) + { + continue; + } + + // determine return fire + if (pSoldier->aiData.bUnderFire && + !pSoldier->bBlindedCounter && + pSoldier->ubPreviousAttackerID == pOpponent->ubID) + { + fReturnFire = TRUE; + } + + bKnowledge = Knowledge(pSoldier, pOpponent->ubID); + bPersonalKnowledge = PersonalKnowledge(pSoldier, pOpponent->ubID); + bPublicKnowledge = PublicKnowledge(pSoldier->bTeam, pOpponent->ubID); + + // check knowledge + if (bKnowledge != SEEN_CURRENTLY && + bKnowledge != SEEN_THIS_TURN && + bKnowledge != SEEN_LAST_TURN && + bKnowledge != HEARD_THIS_TURN && + bKnowledge != HEARD_LAST_TURN && + !((bKnowledge == SEEN_2_TURNS_AGO || bKnowledge == SEEN_3_TURNS_AGO || bKnowledge == HEARD_2_TURNS_AGO) && Weapon[pSoldier->usAttackingWeapon].ubWeaponType == GUN_LMG)) + { + continue; // next opponent + } + + // sevenfm: blind soldier can only attack seen/heard personally + if (pSoldier->bBlindedCounter > 0 && + bPersonalKnowledge != SEEN_THIS_TURN && + bPersonalKnowledge != HEARD_THIS_TURN) + { + continue; // next opponent + } + + // sevenfm: determine if we shoot on unseen target for suppression + if (bPersonalKnowledge != SEEN_CURRENTLY && + bPublicKnowledge != SEEN_CURRENTLY && + //!SoldierToSoldierLineOfSightTest(pSoldier, pOpponent, TRUE, CALC_FROM_ALL_DIRS)) + !LOS_Raised(pSoldier, pOpponent, CALC_FROM_ALL_DIRS)) + { + fSuppression = TRUE; + } + + // sevenfm: shooting at unseen opponents is optional + if (fSuppression && !gGameExternalOptions.fAIShootUnseen) + { + continue; // next opponent + } + + // determine enemy location + if (fSuppression) + { + // personal/public knowledge + sTarget = KnownLocation(pSoldier, pOpponent->ubID); + bLevel = KnownLevel(pSoldier, pOpponent->ubID); + // try to randomize location + sTarget = RandomizeLocation(sTarget, bLevel, 1, pSoldier); + } + else + { + // we know exact enemy location + sTarget = pOpponent->sGridNo; + bLevel = pOpponent->pathing.bLevel; + } + + // safety check + if (TileIsOutOfBounds(sTarget)) + { + continue; + } + + // skip if we can see location and location is empty + if (SoldierToVirtualSoldierLineOfSightTest(pSoldier, sTarget, bLevel, ANIM_PRONE, TRUE, CALC_FROM_ALL_DIRS) && + WhoIsThere2(sTarget, bLevel) == NOBODY) + { + continue; + } + + // no fire on unseen opponents with throwing knives + if ((Item[pSoldier->usAttackingWeapon].usItemClass & IC_THROWING_KNIFE) && + bPersonalKnowledge != SEEN_CURRENTLY && + //!SoldierToSoldierLineOfSightTest(pSoldier, pOpponent, TRUE, CALC_FROM_ALL_DIRS)) + !LOS_Raised(pSoldier, pOpponent, CALC_FROM_ALL_DIRS)) + { + continue; + } + + // sevenfm: only enemy team can use blind suppression fire + if (fSuppression && + pSoldier->bTeam != ENEMY_TEAM) + { + continue; + } + + // sevenfm: only try to suppress alive and conscious human targets + if (fSuppression && + (pOpponent->stats.bLife < OKLIFE || + pOpponent->bCollapsed && pOpponent->bBreath == 0 || + pOpponent->IsCowering() || + pOpponent->IsZombie() || + !IS_MERC_BODY_TYPE(pOpponent))) + { + continue; + } + + // shoot through wall check + if (bPersonalKnowledge != SEEN_CURRENTLY && + //!SoldierToSoldierLineOfSightTest(pSoldier, pOpponent, TRUE, CALC_FROM_ALL_DIRS) && + !LOS_Raised(pSoldier, pOpponent, CALC_FROM_ALL_DIRS) && + !SoldierToVirtualSoldierLineOfSightTest(pSoldier, sTarget, bLevel, ANIM_STAND, TRUE, NO_DISTANCE_LIMIT) && + !LocationToLocationLineOfSightTest(pSoldier->sGridNo, pSoldier->pathing.bLevel, sTarget, bLevel, TRUE, NO_DISTANCE_LIMIT) && + !fReturnFire && + !(InARoom(sTarget, NULL) && bLevel == 0 && Weapon[pSoldier->usAttackingWeapon].ubWeaponType == GUN_LMG) && // bPublicKnowledge == SEEN_CURRENTLY && + !(InARoom(sTarget, NULL) && bLevel == 0 && TeamPercentKilled(pSoldier->bTeam) > (100 - 20 * SoldierDifficultyLevel(pSoldier)))) + { + continue; + } + +#ifdef DEBUGATTACKS + DebugAI(String("%s sees %s at gridno %d\n", pSoldier->GetName(), ExtMen[pOpponent->ubID].GetName(), pOpponent->sGridNo)); +#endif + sMinAPcost = MinAPsToAttack(pSoldier, sTarget, DONTADDTURNCOST, 0); + // later will be decide if shoot is possible this here is just best guess so ignore turnover + + // if we don't have enough APs left to shoot even a snap-shot at this guy + if (sMinAPcost > pSoldier->bActionPoints) + continue; // next opponent + + // sevenfm: check CTGT and friendly fire for each stance instead since they can be different + // calculate chance to get through the opponent's cover (if any) + //dnl ch61 180813 + /*gUnderFire.Clear(); + gUnderFire.Enable(); + ubChanceToGetThrough = AISoldierToSoldierChanceToGetThrough( pSoldier, pOpponent ); + ubFriendlyFireChance = gUnderFire.Chance(pSoldier->bTeam, pSoldier->bSide, TRUE); + gUnderFire.Disable(); + + // if we can't possibly get through all the cover + if (ubChanceToGetThrough == 0) + continue; // next opponent + + // sevenfm: ignore opponent if we can hit friend + if (ubFriendlyFireChance > MIN_CHANCE_TO_ACCIDENTALLY_HIT_SOMEONE) + continue;*/ + + if ((pSoldier->flags.uiStatusFlags & SOLDIER_MONSTER) && (pSoldier->ubBodyType != QUEENMONSTER)) + { + STRUCTURE_FILE_REF* pStructureFileRef; + UINT16 usAnimSurface; + + usAnimSurface = DetermineSoldierAnimationSurface(pSoldier, pSoldier->usUIMovementMode); + pStructureFileRef = GetAnimationStructureRef(pSoldier->ubID, usAnimSurface, pSoldier->usUIMovementMode); + + if (pStructureFileRef) + { + UINT16 usStructureID; + INT8 bDir; + + // must make sure that structure data can be added in the direction of the target + bDir = (INT8)GetDirectionToGridNoFromGridNo(pSoldier->sGridNo, sTarget); + + // ATE: Only if we have a levelnode... + if (pSoldier->pLevelNode != NULL && pSoldier->pLevelNode->pStructureData != NULL) + { + usStructureID = pSoldier->pLevelNode->pStructureData->usStructureID; + } + else + { + usStructureID = INVALID_STRUCTURE_ID; + } + + if (!OkayToAddStructureToWorld(pSoldier->sGridNo, pSoldier->pathing.bLevel, &(pStructureFileRef->pDBStructureRef[gOneCDirection[bDir]]), usStructureID)) + { + // can't turn in that dir.... next opponent + continue; + } + } + } + + iBestHitRate = 0; // reset best hit rate to minimum + + //dnl ch69 130913 Hoping to optimize + // consider alternate holding mode and different scopes + // sevenfm: alt weapon holding scope mode is used only when ubAllowAlternativeWeaponHolding == 3 + for (pSoldier->bScopeMode = (gGameExternalOptions.ubAllowAlternativeWeaponHolding == 3 ? USE_ALT_WEAPON_HOLD : USE_BEST_SCOPE); + pSoldier->bScopeMode <= (gGameExternalOptions.fScopeModes ? NUM_SCOPE_MODES - 1 : USE_BEST_SCOPE); + pSoldier->bScopeMode++) + { + if (pSoldier->bScopeMode == USE_ALT_WEAPON_HOLD) + { + //dnl ch71 180913 throwing knives cannot be used in fire from hip + if (Item[pSoldier->usAttackingWeapon].usItemClass & IC_THROWING_KNIFE) + continue; + + // sevenfm: hip firing allowed only for human bodytypes + if (!IS_MERC_BODY_TYPE(pSoldier)) + continue; + } + + if (pSoldier->bScopeMode == USE_ALT_WEAPON_HOLD || (pSoldier->bScopeMode >= USE_BEST_SCOPE && ObjList[pSoldier->bScopeMode] != NULL)) + { + usTrueState = pSoldier->usAnimState; // because is used in CalculateRaiseGunCost, CalcAimingLevelsAvailableWithAP, CalculateTurningCost + iTrueLastTarget = pSoldier->sLastTarget; // because is used in MinAPsToShootOrStab + + // --------- Standing --------- + ubStance = ANIM_STAND; + // sevenfm: take into account direction when checking stance + // sevenfm: shoot heavy guns in standing stance only when using hip fire + if (pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, sTarget), ubStance) && + (pSoldier->bScopeMode == USE_ALT_WEAPON_HOLD || !Weapon[pSoldier->usAttackingWeapon].HeavyGun || !ItemIsTwoHanded(pSoldier->usAttackingWeapon) || !gGameExternalOptions.ubAllowAlternativeWeaponHolding)) + { + sStanceAPcost = GetAPsToChangeStance(pSoldier, ubStance); + if (sStanceAPcost) + { + // Going up so first is stance change then turnover, do animation change before APs calculation + pSoldier->usAnimState = STANDING; + pSoldier->sLastTarget = NOWHERE; + } + GetAPChargeForShootOrStabWRTGunRaises(pSoldier, sTarget, TRUE, &fAddingTurningCost, &fAddingRaiseGunCost, 0); + usTurningCost = CalculateTurningCost(pSoldier, pSoldier->usAttackingWeapon, fAddingTurningCost); + usRaiseGunCost = CalculateRaiseGunCost(pSoldier, fAddingRaiseGunCost, sTarget, 0); + if (fAddingTurningCost && fAddingRaiseGunCost)//dnl ch71 180913 + { + if (usRaiseGunCost > usTurningCost) + usTurningCost = 0; + else + usRaiseGunCost = 0; + } + sRawAPCost = MinAPsToShootOrStab(pSoldier, sTarget, 0, FALSE, 2); + sMinAPcost = sRawAPCost + usTurningCost + sStanceAPcost + usRaiseGunCost; + + if (pSoldier->bActionPoints - sMinAPcost >= 0) + { + // calc next attack's minimum shooting cost (excludes readying & turning & raise gun) + sMaxPossibleAimTime = CalcAimingLevelsAvailableWithAP(pSoldier, sTarget, pSoldier->bActionPoints - sMinAPcost); + + // sevenfm: check CTGT and friendly fire chance for every stance + gUnderFire.Clear(); + gUnderFire.Enable(); + if (fSuppression) + ubChanceToGetThrough = SoldierToLocationChanceToGetThrough(pSoldier, sTarget, bLevel, 3, NOBODY); + else + ubChanceToGetThrough = SoldierToSoldierChanceToGetThrough(pSoldier, pOpponent); + ubFriendlyFireChance = gUnderFire.Chance(pSoldier->bTeam, pSoldier->bSide, TRUE); + gUnderFire.Disable(); + + // sevenfm: only use this stance if we can hit target and cannot hit friends + if (ubChanceToGetThrough > 0 && ubFriendlyFireChance <= MIN_CHANCE_TO_ACCIDENTALLY_HIT_SOMEONE) + { + for (sAimTime = 0; sAimTime <= sMaxPossibleAimTime; sAimTime++) + { + sChanceToHit = AICalcChanceToHitGun(pSoldier, sTarget, sAimTime, AIM_SHOT_TORSO, bLevel, STANDING); + sAimAPCost = CalcAPCostForAiming(pSoldier, sTarget, (INT8)sAimTime); + iHitRate = sChanceToHit * (pSoldier->bActionPoints - (sMinAPcost - sRawAPCost)) / (sRawAPCost + sAimAPCost); + // sevenfm: take into account CTGT for every stance + if (iHitRate * ubChanceToGetThrough > iBestHitRate * ubBestChanceToGetThrough || + (Item[pSoldier->usAttackingWeapon].usItemClass & IC_THROWING_KNIFE) && sChanceToHit > sBestChanceToHit)// rather take best chance for throwing knives + { + iBestHitRate = iHitRate; + sBestAimTime = sAimTime; + sBestChanceToHit = sChanceToHit; + ubBestChanceToGetThrough = ubChanceToGetThrough; + ubBestFriendlyFireChance = ubFriendlyFireChance; + bScopeMode = pSoldier->bScopeMode; + sBestAPcost = sMinAPcost; + ubBestStance = ubStance; + } + } + } + } + pSoldier->usAnimState = usTrueState; + pSoldier->sLastTarget = iTrueLastTarget; + } + + // no crouched/prone if we are tank/using throwing knife/hip firing + if (pSoldier->bScopeMode == USE_ALT_WEAPON_HOLD || ARMED_VEHICLE(pSoldier) || ENEMYROBOT(pSoldier) || (Item[pSoldier->usAttackingWeapon].usItemClass & IC_THROWING_KNIFE)) + continue; + + // --------- Crouched --------- + ubStance = ANIM_CROUCH; + // sevenfm: take into account direction + if (pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, sTarget), ubStance)) + { + // change stance then turn + sStanceAPcost = GetAPsToChangeStance(pSoldier, ubStance); + if (sStanceAPcost) + { + pSoldier->usAnimState = CROUCHING; + pSoldier->sLastTarget = NOWHERE; + } + GetAPChargeForShootOrStabWRTGunRaises(pSoldier, sTarget, TRUE, &fAddingTurningCost, &fAddingRaiseGunCost, 0); + usTurningCost = CalculateTurningCost(pSoldier, pSoldier->usAttackingWeapon, fAddingTurningCost); + usRaiseGunCost = CalculateRaiseGunCost(pSoldier, fAddingRaiseGunCost, sTarget, 0); + if (fAddingTurningCost && fAddingRaiseGunCost)//dnl ch71 180913 + { + if (usRaiseGunCost > usTurningCost) + usTurningCost = 0; + else + usRaiseGunCost = 0; + } + sRawAPCost = MinAPsToShootOrStab(pSoldier, sTarget, 0, FALSE, 2); + sMinAPcost = sRawAPCost + usTurningCost + sStanceAPcost + usRaiseGunCost; + + if (pSoldier->bActionPoints - sMinAPcost >= 0) + { + sMaxPossibleAimTime = CalcAimingLevelsAvailableWithAP(pSoldier, sTarget, pSoldier->bActionPoints - sMinAPcost); + + // sevenfm: check CTGT and friendly fire chance for every stance + gUnderFire.Clear(); + gUnderFire.Enable(); + if (fSuppression) + ubChanceToGetThrough = SoldierToLocationChanceToGetThrough(pSoldier, sTarget, bLevel, 3, NOBODY); + else + ubChanceToGetThrough = SoldierToSoldierChanceToGetThrough(pSoldier, pOpponent); + ubFriendlyFireChance = gUnderFire.Chance(pSoldier->bTeam, pSoldier->bSide, TRUE); + gUnderFire.Disable(); + + // sevenfm: only use this stance if we can hit target and cannot hit friends + if (ubChanceToGetThrough > 0 && ubFriendlyFireChance <= MIN_CHANCE_TO_ACCIDENTALLY_HIT_SOMEONE) + { + for (sAimTime = 0; sAimTime <= sMaxPossibleAimTime; sAimTime++) + { + sChanceToHit = AICalcChanceToHitGun(pSoldier, sTarget, sAimTime, AIM_SHOT_TORSO, bLevel, CROUCHING); + sAimAPCost = CalcAPCostForAiming(pSoldier, sTarget, (INT8)sAimTime); + iHitRate = sChanceToHit * (pSoldier->bActionPoints - (sMinAPcost - sRawAPCost)) / (sRawAPCost + sAimAPCost); + // sevenfm: take into account CTGT for every stance + if (iHitRate * ubChanceToGetThrough > iBestHitRate * ubBestChanceToGetThrough) + { + iBestHitRate = iHitRate; + sBestAimTime = sAimTime; + sBestChanceToHit = sChanceToHit; + ubBestChanceToGetThrough = ubChanceToGetThrough; + ubBestFriendlyFireChance = ubFriendlyFireChance; + bScopeMode = pSoldier->bScopeMode; + sBestAPcost = sMinAPcost; + ubBestStance = ubStance; + } + } + } + } + pSoldier->usAnimState = usTrueState; + pSoldier->sLastTarget = iTrueLastTarget; + } + + // no prone stance if we have to change direction and stance at the same time + if (pSoldier->ubDirection != AIDirection(pSoldier->sGridNo, sTarget) && + gAnimControl[pSoldier->usAnimState].ubEndHeight > ANIM_PRONE) + { + continue; + } + + // --------- Prone --------- + ubStance = ANIM_PRONE; + if (pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, sTarget), ubStance)) + { + sStanceAPcost = GetAPsToChangeStance(pSoldier, ubStance); + if (sStanceAPcost) + { + pSoldier->usAnimState = PRONE; + pSoldier->sLastTarget = NOWHERE; + } + GetAPChargeForShootOrStabWRTGunRaises(pSoldier, sTarget, TRUE, &fAddingTurningCost, &fAddingRaiseGunCost, 0); + usTurningCost = CalculateTurningCost(pSoldier, pSoldier->usAttackingWeapon, fAddingTurningCost); + usRaiseGunCost = CalculateRaiseGunCost(pSoldier, fAddingRaiseGunCost, sTarget, 0); + sRawAPCost = MinAPsToShootOrStab(pSoldier, sTarget, 0, FALSE, 2); + sMinAPcost = sRawAPCost + usTurningCost + sStanceAPcost + usRaiseGunCost; + + if (pSoldier->bActionPoints - sMinAPcost >= 0) + { + sMaxPossibleAimTime = CalcAimingLevelsAvailableWithAP(pSoldier, sTarget, pSoldier->bActionPoints - sMinAPcost); + + // sevenfm: check CTGT and friendly fire chance for every stance + gUnderFire.Clear(); + gUnderFire.Enable(); + if (fSuppression) + ubChanceToGetThrough = SoldierToLocationChanceToGetThrough(pSoldier, sTarget, bLevel, 3, NOBODY); + else + ubChanceToGetThrough = SoldierToSoldierChanceToGetThrough(pSoldier, pOpponent); + ubFriendlyFireChance = gUnderFire.Chance(pSoldier->bTeam, pSoldier->bSide, TRUE); + gUnderFire.Disable(); + + // sevenfm: only use this stance if we can hit target and cannot hit friends + if (ubChanceToGetThrough > 0 && ubFriendlyFireChance <= MIN_CHANCE_TO_ACCIDENTALLY_HIT_SOMEONE) + { + for (sAimTime = 0; sAimTime <= sMaxPossibleAimTime; sAimTime++) + { + sChanceToHit = AICalcChanceToHitGun(pSoldier, sTarget, sAimTime, AIM_SHOT_TORSO, bLevel, PRONE); + sAimAPCost = CalcAPCostForAiming(pSoldier, sTarget, (INT8)sAimTime); + iHitRate = sChanceToHit * (pSoldier->bActionPoints - (sMinAPcost - sRawAPCost)) / (sRawAPCost + sAimAPCost); + // sevenfm: take into account CTGT for every stance + if (iHitRate * ubChanceToGetThrough > iBestHitRate * ubBestChanceToGetThrough) + { + iBestHitRate = iHitRate; + sBestAimTime = sAimTime; + sBestChanceToHit = sChanceToHit; + ubBestChanceToGetThrough = ubChanceToGetThrough; + ubBestFriendlyFireChance = ubFriendlyFireChance; + bScopeMode = pSoldier->bScopeMode; + sBestAPcost = sMinAPcost; + ubBestStance = ubStance; + } + } + } + } + pSoldier->usAnimState = usTrueState; + pSoldier->sLastTarget = iTrueLastTarget; + } + } + } + + // if we can't get any kind of hit rate at all + if (iBestHitRate == 0) + continue; // next opponent + + // calculate chance to REALLY hit: shoot accurately AND get past cover + ubChanceToReallyHit = (UINT8)ceil((sBestChanceToHit * ubBestChanceToGetThrough) / 100.0f); + + // if we can't REALLY hit at all + if (ubChanceToReallyHit == 0) + continue; // next opponent + + // really limit knife throwing so it doesn't look wrong + if (Item[pSoldier->usAttackingWeapon].usItemClass == IC_THROWING_KNIFE && + (ubChanceToReallyHit < 25 || (PythSpacesAway(pSoldier->sGridNo, sTarget) > CalcMaxTossRange(pSoldier, pSoldier->usAttackingWeapon, FALSE))))// Madd / 2 ) ) ) //dnl ch69 160913 was ubChanceToReallyHit < 30 + continue; // don't bother... next opponent + + // calculate this opponent's threat value (factor in my cover from him) + iThreatValue = CalcManThreatValue(pOpponent, pSoldier->sGridNo, TRUE, pSoldier); + + // estimate the damage this shot would do to this opponent + iEstDamage = EstimateShotDamage(pSoldier, pOpponent, sBestChanceToHit); + //NumMessage("SHOT EstDamage = ",iEstDamage); + + // calculate the combined "attack value" for this opponent + // highest possible value before division should be about 1.8 billion... + // normal value before division should be about 5 million... + iAttackValue = (iEstDamage * iBestHitRate * ubChanceToReallyHit * iThreatValue) / 1000; + //NumMessage("SHOT AttackValue = ",iAttackValue / 1000); + + // sevenfm: penalize suppression fire + if (fSuppression) + { + // 25% penalty for shooting at invisible target + iAttackValue = iAttackValue / 2; + } + + // special stuff for assassins to ignore militia more + if (pSoldier->IsAssassin() && pOpponent->bTeam == MILITIA_TEAM) + { + iAttackValue /= 2; + } + + // sevenfm: empty vehicles have very low priority + if (pOpponent->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle(pOpponent->bVehicleID) == 0) + { + iAttackValue /= 4; + } + + // sevenfm: dying, cowering or unconscious soldiers have very low priority + if (pOpponent->stats.bLife < OKLIFE || pOpponent->bCollapsed || pOpponent->bBreathCollapsed) + { + iAttackValue /= 4; + } + +#ifdef DEBUGATTACKS + DebugAI(String("CalcBestShot: best AttackValue vs %d = %d\n", uiLoop, iAttackValue)); +#endif + + // if we can hurt the guy, OR probably not, but at least it's our best + // chance to actually hit him and maybe scare him, knock him down, etc. + if ((iAttackValue > 0) || (ubChanceToReallyHit > pBestShot.ubChanceToReallyHit)) + { +#if 0 // if there already was another viable target + if (pBestShot.ubChanceToReallyHit > 0) + { + // OK, how does our chance to hit him compare to the previous best one? + iPercentBetter = ((ubChanceToReallyHit * 100) / pBestShot.ubChanceToReallyHit) - 100; + + //dnl ch62 180813 ignore firing into breathless targets if there are targets in better condition + // sevenfm: check that best opponent exists + if (pBestShot.ubOpponent != NOBODY && + (Menptr[pBestShot.ubOpponent].bCollapsed || Menptr[pBestShot.ubOpponent].bBreathCollapsed) && + Menptr[pBestShot.ubOpponent].bBreath < OKBREATH + && Menptr[pBestShot.ubOpponent].bBreath < pOpponent->bBreath) + { + iPercentBetter = PERCENT_TO_IGNORE_THREAT; + } + + // sevenfm: if best opponent is dying and new opponent is ok, use new opponent + if (pBestShot.ubOpponent != NOBODY && + Menptr[pBestShot.ubOpponent].stats.bLife < OKLIFE && + pOpponent->stats.bLife >= OKLIFE) + { + iPercentBetter = PERCENT_TO_IGNORE_THREAT; + } + + // if this chance to really hit is more than 50% worse, and the other + // guy is conscious at all + if (iPercentBetter < -PERCENT_TO_IGNORE_THREAT && + pBestShot.ubOpponent != NOBODY && + Menptr[pBestShot.ubOpponent].stats.bLife >= OKLIFE) + { + // then stick with the older guy as the better target + continue; + } + + // if this chance to really hit between 50% worse to 50% better + if (iPercentBetter < PERCENT_TO_IGNORE_THREAT) + { + // then the one with the higher ATTACK VALUE is the better target + if (iAttackValue < pBestShot.iAttackValue) + // the previous guy is more important since he's more dangerous + continue; // next opponent + } + } + + // sevenfm: if new opponent is dying and best opponent is ok, ignore new opponent + if (pBestShot.ubOpponent != NOBODY && + Menptr[pBestShot.ubOpponent].stats.bLife >= OKLIFE && + pOpponent->stats.bLife < OKLIFE) + { + //DebugShot(pSoldier, String("new opponent is dying, best opponent is ok - skip")); + continue; + } +#endif + // OOOF! That was a lot of work! But we've got a new viable target! + pBestShot.ubPossible = TRUE; + pBestShot.ubOpponent = pOpponent->ubID; + pBestShot.ubAimTime = sBestAimTime; + pBestShot.ubChanceToReallyHit = ubChanceToReallyHit; + pBestShot.sTarget = sTarget; + pBestShot.bTargetLevel = bLevel; + pBestShot.iAttackValue = iAttackValue; + pBestShot.ubAPCost = sBestAPcost; + pBestShot.ubStance = ubBestStance; + pBestShot.bScopeMode = bScopeMode; + pBestShot.ubFriendlyFireChance = ubBestFriendlyFireChance; + + possibleShots.push_back(pBestShot); + } + } + + pSoldier->bScopeMode = USE_BEST_SCOPE; // better reset this back +} + +bool CompareAttacks(const ATTACKTYPE& a, const ATTACKTYPE& b) +{ + if (a.iAttackValue > b.iAttackValue) + return true; + else if (a.iAttackValue < b.iAttackValue) + return false; + + if (a.ubChanceToReallyHit > b.ubChanceToReallyHit) + return true; + else if (a.ubChanceToReallyHit < b.ubChanceToReallyHit) + return false; + + if (a.ubFriendlyFireChance < b.ubFriendlyFireChance) + return true; + else if (a.ubFriendlyFireChance > b.ubFriendlyFireChance) + return false; + + if (a.ubAPCost < b.ubAPCost) + return true; + else if (a.ubAPCost > b.ubAPCost) + return false; + + return false; +} + +void CheckIfShotsPossible(SOLDIERTYPE* pSoldier, std::vector& possibleShots) +{ + INT8 guns[] = { FindAIUsableObjClass(pSoldier, IC_GUN), FindAIUsableObjClass(pSoldier, IC_GUN, TRUE) }; + // Only merc bodytypes can use a sidearm, also don't bother to check for targets if found sidearm is the same as main gun + if (!IS_MERC_BODY_TYPE(pSoldier) || guns[0] == guns[1]) + { + guns[1] = NO_SLOT; + } + + for (size_t i = 0; i < 2; i++) + { + const auto gun = guns[i]; + // if the soldier does have a gun + if (gun != NO_SLOT) + { + // if it's in his holster, swap it into his hand temporarily + if (gun != HANDPOS) + { + RearrangePocket(pSoldier, HANDPOS, gun, TEMPORARILY); + } + + // get the minimum cost to attack with this item + INT16 ubMinAPcost = MinAPsToAttack(pSoldier, pSoldier->sLastTarget, ADDTURNCOST, 0); + + // if we can afford the minimum AP cost + if (pSoldier->bActionPoints >= ubMinAPcost) + { + // then look around for worthy targets + CalculatePossibleShots(pSoldier, gun, possibleShots); + } + + // if it was in his holster, swap it back into his holster for now + if (gun != HANDPOS) + { + RearrangePocket(pSoldier, HANDPOS, gun, TEMPORARILY); + } + } + } +} diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index bc6aee600..474ee9c1d 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -51,6 +51,127 @@ void LogKnowledgeInfo(SOLDIERTYPE *pSoldier, bool doLog); INT8 DecideActionWearGasmask(SOLDIERTYPE* pSoldier); ActionType DecideActionStuckInWaterOrGas(SOLDIERTYPE* pSoldier, BOOLEAN ubCanMove, BOOLEAN bInWater, BOOLEAN bInDeepWater, BOOLEAN bInGas); +////////////////////////////////////////////////////////////////////////////// +// Utility functions for decisions +////////////////////////////////////////////////////////////////////// +static INT32 currentHealthSituation(INT8 HP, INT8 maxHP) +{ + float shift = 0.3 * maxHP; + float tilt = 10 * HP; + float y = 1 / (((HP - shift) * (HP - shift) + tilt) / maxHP * maxHP); + auto chance = max(0, static_cast(y)); + + return min(chance, 100); +} + +// Based on Behavioral Mathematics for Game AI +// Edges give a weighted probability that a random choice lies between them. +// +// +// RandomChoice(maximum Edge) -> 30 +// | +// | +// | +// 0 3 20 * 45 +// | | | |<--- Edges +// | | | | +// |__|______|__________| +// {__] +// ^ {______] +// | ^ {__________] +// | | ^ +// | | | +// | | Option 3 +// | | +// | Option 2 +// | +// | Option 1 +// +INT32 DoWeightedChoice(std::vector& edges) +{ + auto maxEdge = edges[edges.size() - 1]; + + auto choice = Random(maxEdge); + for (size_t i = 0; i < edges.size(); i++) + { + if (i == 0 && choice <= edges[i]) + { + return i; + } + else if (edges[i - 1] < choice && choice <= edges[i]) + { + return i; + } + } +} + +void CreateEdges(std::vector &fweights, float weight_min, std::vector& edges) +{ + unsigned int currentEdge = 0; + for (size_t i = 0; i < fweights.size(); i++) + { + auto weight = fweights[i] / weight_min; // Scale weight so we can get useable integers + auto newEdge = currentEdge + weight; + edges.push_back(newEdge); + currentEdge = newEdge; + } +} + +float BeneficialScore(INT32 value, INT32 maxValue, float clampValue) +{ + float weight = max(static_cast(value) / maxValue, clampValue); + return weight; +} + +float NonBeneficialScore(INT32 value, INT32 minValue, float clampValue) +{ + float weight = max(static_cast(minValue) / max(value, 1), clampValue); + return weight; +} + +void WeighAttacks(std::vector& possibleShots, std::vector& edges) +{ + auto maxAttack = possibleShots[0].iAttackValue; + auto maxCTH = possibleShots[0].ubChanceToReallyHit; + auto minAPCost = possibleShots[0].ubAPCost; + auto minFFchance = possibleShots[0].ubFriendlyFireChance; + + for (size_t i = 1; i < possibleShots.size(); i++) + { + maxAttack = max(maxAttack, possibleShots[i].iAttackValue); + maxCTH = max(maxCTH, possibleShots[i].ubChanceToReallyHit); + minAPCost = min(minAPCost, possibleShots[i].ubAPCost); + minFFchance = min(minFFchance, possibleShots[i].ubFriendlyFireChance); + } + // Clamp values to prevent divide by zero + maxAttack = max(maxAttack, 1); + maxCTH = max(maxCTH, 1); + minAPCost = max(minAPCost, 1); + minFFchance = max(minFFchance, 1); + + + std::vector fweights{}; + float weight_min = FLT_MAX; + for (size_t i = 0; i < possibleShots.size(); i++) + { + auto& shot = possibleShots[i]; + const auto clampValue = 0.0001f; // To prevent 0 value weights + + float attackWeight = BeneficialScore(shot.iAttackValue, maxAttack, clampValue); + float cthWeight = BeneficialScore(shot.ubChanceToReallyHit, maxCTH, clampValue); + float apcostWeight = NonBeneficialScore(shot.ubAPCost, minAPCost, clampValue); + float ffChanceWeight = NonBeneficialScore(shot.ubFriendlyFireChance, minFFchance, clampValue); + + auto weight_i = attackWeight * cthWeight * apcostWeight * ffChanceWeight; + weight_min = min(weight_min, weight_i); + fweights.push_back(weight_i); + } + + CreateEdges(fweights, weight_min, edges); +} + +////////////////////////////////////////////////////////////////////// + // global status time counters to determine what takes the most time #ifdef AI_TIMING_TESTS @@ -2445,7 +2566,7 @@ INT8 DecideActionYellow(SOLDIERTYPE *pSoldier) INT8 DecideActionRed(SOLDIERTYPE *pSoldier) { - INT8 bActionReturned; + ActionType bActionReturned; INT32 iDummy; INT32 iChance; INT32 sClosestDisturbance = NOWHERE, sCheckGridNo; @@ -2560,10 +2681,10 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // do some special panic AI decision making bActionReturned = PanicAI(pSoldier,ubCanMove); - // if we decided on an action while in there, we're done - if (bActionReturned != -1) - return(bActionReturned); - } + // if we decided on an action while in there, we're done + if (bActionReturned != AI_ACTION_INVALID) + return(bActionReturned); + } if ( pSoldier->ubProfile != NO_PROFILE ) { @@ -5081,7 +5202,8 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) INT16 ubMinAPCost; INT8 bDirection; UINT8 ubBestAttackAction = AI_ACTION_NONE; - INT8 bCanAttack,bActionReturned; + INT8 bCanAttack; + ActionType bActionReturned; INT8 bWeaponIn; BOOLEAN fTryPunching = FALSE; #ifdef DEBUGDECISIONS @@ -5178,7 +5300,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) bActionReturned = PanicAI(pSoldier,ubCanMove); // if we decided on an action while in there, we're done - if (bActionReturned != -1) + if (bActionReturned != AI_ACTION_INVALID) return(bActionReturned); } @@ -5280,7 +5402,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) // STUCK IN WATER OR GAS, NO COVER, GO TO NEAREST SPOT OF UNGASSED LAND //////////////////////////////////////////////////////////////////////////// auto decision = DecideActionStuckInWaterOrGas(pSoldier, ubCanMove, bInWater, bInDeepWater, bInGas); - if (decision != AI_ACTION_LAST) + if (decision != AI_ACTION_INVALID) { return decision; } @@ -10677,5 +10799,11556 @@ ActionType DecideActionStuckInWaterOrGas(SOLDIERTYPE *pSoldier, BOOLEAN ubCanMov } - return AI_ACTION_LAST; + return AI_ACTION_INVALID; +} + +//////////////////////////////////////////////////////////////////////////// +// RADIO RED ALERT: determine %chance to call others and report contact +//////////////////////////////////////////////////////////////////////////// +static ActionType DecideActionRadioRedAlert(SOLDIERTYPE* pSoldier, bool logAction) +{ + // if we're a computer merc, and we have the action points remaining to RADIO + // (we never want NPCs to choose to radio if they would have to wait a turn) + auto haveNotUsedRadio = !(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT); + auto enoughActionPoints = pSoldier->bActionPoints >= APBPConstants[AP_RADIO]; + auto haveTeammates = gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1; + + if (haveNotUsedRadio && enoughActionPoints && haveTeammates) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Radio red alert]"), logAction); + + INT32 iChance; + auto weAreNotAwareOfEnemy = !(gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition); + if (weAreNotAwareOfEnemy || NeedToRadioAboutPanicTrigger()) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Radio first red alert!"), logAction); + // since I'm at STATUS RED, I obviously know we're being invaded! + iChance = gbDiff[DIFF_RADIO_RED_ALERT][SoldierDifficultyLevel(pSoldier)]; + } + else // subsequent radioing (only to update enemy positions, request help) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Subsequent radio alert"), logAction); + // base chance depends on how much new info we have to radio to the others + iChance = 10 * WhatIKnowThatPublicDont(pSoldier, FALSE); // use 10 * for RED alert + } + + // if I actually know something they don't + DebugAI(AI_MSG_INFO, pSoldier, String("Do I know more than others? %d", iChance), logAction); + if (iChance) + { + // modify base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += 20; break; + case ONGUARD: iChance += 15; break; + case ONCALL: iChance += 10; break; + case CLOSEPATROL: break; + case RNDPTPATROL: + case POINTPATROL: iChance += -5; break; + case FARPATROL: iChance += -10; break; + case SEEKENEMY: iChance += -20; break; + case SNIPER: iChance += -10; break; // Sniper contacts should be reported automatically + } + + // modify base chance according to attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += 20; break; + case BRAVESOLO: iChance += -10; break; + case BRAVEAID: break; + case CUNNINGSOLO: iChance += -5; break; + case CUNNINGAID: break; + case AGGRESSIVE: iChance += -20; break; + case ATTACKSLAYONLY: iChance = 0; break; + } + + auto panicTriggerIsPresent = (gTacticalStatus.fPanicFlags & PANIC_TRIGGERS_HERE); + if ( panicTriggerIsPresent && weAreNotAwareOfEnemy) + { + // ignore morale (which could be really high + } + else + { + // modify base chance according to morale + switch (pSoldier->aiData.bAIMorale) + { + case MORALE_HOPELESS: iChance *= 3; break; + case MORALE_WORRIED: iChance *= 2; break; + case MORALE_NORMAL: break; + case MORALE_CONFIDENT: iChance /= 2; break; + case MORALE_FEARLESS: iChance /= 3; break; + } + } + + // TODO: This was in original code for status BLACK, check if radioing is excessive without it + // reduce chance because we're in combat + // iChance /= 2; + + DebugAI(AI_MSG_INFO, pSoldier, String("Chance to radio alert = %d", iChance), logAction); + if ((INT16)PreRandom(100) < iChance) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Decided to radio red alert"), logAction); + return(AI_ACTION_RED_ALERT); + } + } + } + + return(AI_ACTION_INVALID); +} + +//////////////////////////////////////////////////////////////////////////// +// RADIO OPERATOR TRAIT +//////////////////////////////////////////////////////////////////////////// +static ActionType DecideActionRadioOperator(SOLDIERTYPE* pSoldier, bool logAction) +{ + if (HAS_SKILL_TRAIT(pSoldier, RADIO_OPERATOR_NT) > 0 && pSoldier->CanUseSkill(SKILLS_RADIO_ARTILLERY, TRUE)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Radio operator]"), logAction); + // check: would it be possible to call in artillery from neighbouring sectors? + UINT32 tmp; + INT32 skilltargetgridno = 0; + // can we call in artillery? + if (pSoldier->CanAnyArtilleryStrikeBeOrdered(&tmp)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Attempt to call reinforcements"), logAction); + // if frequencies are jammed... + if (SectorJammed()) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Someone's jamming radio!"), logAction); + // if we are jamming, turn it off, otherwise, bad luck... + if (pSoldier->IsJamming()) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Turn off radio jamming..."), logAction); + pSoldier->usAISkillUse = SKILLS_RADIO_TURNOFF; + pSoldier->aiData.usActionData = skilltargetgridno; + return(AI_ACTION_USE_SKILL); + } + } + // frequencies are clear, order a strike + else if (GetBestAoEGridNo(pSoldier, &skilltargetgridno, max(1, gSkillTraitValues.usVOMortarRadius - 2), 1, 2, SoldierCondTrue, SoldierCondFalse)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Order an artillery strike!"), logAction); + pSoldier->usAISkillUse = SKILLS_RADIO_ARTILLERY; + pSoldier->aiData.usActionData = skilltargetgridno; + return(AI_ACTION_USE_SKILL); + } + } + // no access to artillery... we can still call reinforcements if we haven't yet done so + else if (!gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition && MoreFriendsThanEnemiesinNearbysectors(pSoldier->bTeam, pSoldier->sSectorX, pSoldier->sSectorY, pSoldier->bSectorZ)) + { + // if frequencies are jammed... + if (SectorJammed()) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Someone's jamming radio!"), logAction); + // if we are jamming, turn it off, otherwise, bad luck... + if (pSoldier->IsJamming()) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Turn off radio jamming..."), logAction); + pSoldier->usAISkillUse = SKILLS_RADIO_TURNOFF; + pSoldier->aiData.usActionData = skilltargetgridno; + return(AI_ACTION_USE_SKILL); + } + } + // frequencies are clear, lets call for help + else if (!(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT)) + { + // raise alarm! + DebugAI(AI_MSG_INFO, pSoldier, String("Call for reinforcements!"), logAction); + return(AI_ACTION_RED_ALERT); + } + } + // if we can't call in artillery or reinforcements, then nobody else from our team can. So we better jam communications, so that the player cannot use these skills either + else if (!pSoldier->IsJamming()) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Start jamming radio frequencies"), logAction); + pSoldier->usAISkillUse = SKILLS_RADIO_JAM; + pSoldier->aiData.usActionData = skilltargetgridno; + return(AI_ACTION_USE_SKILL); + } + } + + return(AI_ACTION_INVALID); +} + +//////////////////////////////////////////////////////////////////////////// +// TURN TO FACE CLOSEST KNOWN OPPONENT (IF NOT FACING THERE ALREADY) +//////////////////////////////////////////////////////////////////////////// +static ActionType DecideActionChangeFacing(SOLDIERTYPE* pSoldier, UINT8 ubCanMove, bool logAction) +{ + if (!gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Turn to closest known opponent]"), logAction); + // hopeless guys shouldn't waste their time this way, UNLESS they CAN move + // but chose not to to get this far (which probably means they're cornered) + // ALSO, don't bother turning if we're already aiming a gun + if (!gfHiddenInterrupt && ((pSoldier->aiData.bAIMorale > MORALE_HOPELESS) || ubCanMove)) + { + // determine the location of the known closest opponent + // (don't care if he's conscious, don't care if he's reachable at all) + + + auto sClosestOpponent = ClosestSeenOpponent(pSoldier, NULL, NULL); + // if we have a closest reachable opponent + if (!TileIsOutOfBounds(sClosestOpponent)) + { + if (!TileIsOutOfBounds(pSoldier->sLastTarget)) + sClosestOpponent = pSoldier->sLastTarget; + INT8 bDirection = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestOpponent); + + // if we're not facing towards him + if (pSoldier->ubDirection != bDirection && pSoldier->InternalIsValidStance(bDirection, gAnimControl[pSoldier->usAnimState].ubEndHeight)) + { + pSoldier->aiData.usActionData = bDirection; + DebugAI(AI_MSG_TOPIC, pSoldier, String("face closest opponent in direction %d", pSoldier->aiData.usActionData), logAction); + return(AI_ACTION_CHANGE_FACING); + } + } + } + } + + return(AI_ACTION_INVALID); +} + +//////////////////////////////////////////////////////////////////////////// +// VIP RETREAT +//////////////////////////////////////////////////////////////////////////// +// VIPs run away (but not the GENERAL) +static ActionType DecideActionVIPretreat(SOLDIERTYPE* pSoldier, bool logAction) +{ + if (ISVIP(pSoldier) && pSoldier->ubProfile != GENERAL) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[VIP Retreat]"), logAction); + // this is in red AI state - a firefight is going on, we try to escape + pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); + + // if we don't know where our opponents are, we cannot run away from them... + if (TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + // search for the closest map edge + DebugAI(AI_MSG_INFO, pSoldier, String("Don't know where enemies are, head for nearest map edge"), logAction); + pSoldier->aiData.usActionData = FindClosestExitGrid(pSoldier, pSoldier->sGridNo, 200); + } + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("[VIP Retreat] grid# %d", pSoldier->aiData.usActionData), logAction); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d VIP retreat. Target grid %d", pSoldier->ubID.i, pSoldier->aiData.usActionData); + return AI_ACTION_RUN_AWAY; + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("No valid gridno found! Tried to head for gridno %d", pSoldier->aiData.usActionData), logAction); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d VIP retreat. No valid grid found! Tried to head for grid %d", pSoldier->ubID.i, pSoldier->aiData.usActionData); + } + } + + return(AI_ACTION_INVALID); +} + +//////////////////////////////////////////////////////////////////////////// +// CHANGE STANCE +//////////////////////////////////////////////////////////////////////////// +ActionType DecideActionChangeStance(SOLDIERTYPE* pSoldier, UINT8 ubCanMove, ATTACKTYPE BestAttack, UINT8 ubBestAttackAction, bool logAction) +{ + // if not in water and not already crouched, try to crouch down first + if (!gfTurnBasedAI || GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Change stance]"), logAction); + if (!gfHiddenInterrupt && IsValidStance(pSoldier, ANIM_CROUCH) && ubBestAttackAction != AI_ACTION_KNIFE_MOVE && ubBestAttackAction != AI_ACTION_KNIFE_STAB && ubBestAttackAction != AI_ACTION_STEAL_MOVE) // SANDRO - if knife attack don't crouch + { + // determine the location of the known closest opponent + // (don't care if he's conscious, don't care if he's reachable at all) + + INT32 sClosestOpponent = ClosestSeenOpponent(pSoldier, NULL, NULL); + // SANDRO - don't crouch if in close combat distance (we got penalties that way) + if (PythSpacesAway(pSoldier->sGridNo, sClosestOpponent) > 1) + { + pSoldier->aiData.usActionData = StanceChange(pSoldier, BestAttack.ubAPCost); + if (pSoldier->aiData.usActionData != 0) + { + if (pSoldier->aiData.usActionData == ANIM_PRONE) + { + // we might want to turn before lying down! + if ((!gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints - GetAPsToChangeStance(pSoldier, (INT8)pSoldier->aiData.usActionData)) && + ((pSoldier->aiData.bAIMorale > MORALE_HOPELESS) || ubCanMove)) + { + // if we have a closest seen opponent + if (!TileIsOutOfBounds(sClosestOpponent)) + { + INT8 bDirection = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestOpponent); + + // if we're not facing towards him + if (pSoldier->ubDirection != bDirection) + { + if (pSoldier->InternalIsValidStance(bDirection, (INT8)pSoldier->aiData.usActionData)) + { + // change direction, THEN change stance! + pSoldier->aiData.bNextAction = AI_ACTION_CHANGE_STANCE; + pSoldier->aiData.usNextActionData = pSoldier->aiData.usActionData; + pSoldier->aiData.usActionData = bDirection; + return(AI_ACTION_CHANGE_FACING); + } + else if ((pSoldier->aiData.usActionData == ANIM_PRONE) && (pSoldier->InternalIsValidStance(bDirection, ANIM_CROUCH))) + { + // we shouldn't go prone, since we can't turn to shoot + pSoldier->aiData.usActionData = ANIM_CROUCH; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_CHANGE_STANCE); + } + } + // else we are facing in the right direction + + } + // else we don't know any enemies + } + + // we don't want to turn + } + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_CHANGE_STANCE); + } + } + } + } + + return(AI_ACTION_INVALID); +} + +static ActionType MoveCloserBeforeShooting(SOLDIERTYPE* pSoldier, ATTACKTYPE attack) +{ + const auto sClosestOpponent = attack.ubOpponent->sGridNo; + + if ( !TileIsOutOfBounds(sClosestOpponent) ) + { + // temporarily make merc get closer reserving enough for expected cost of shot + USHORT tgrd = pSoldier->aiData.sPatrolGrid[0]; + INT8 oldOrders = pSoldier->aiData.bOrders; + pSoldier->aiData.sPatrolGrid[0] = pSoldier->sGridNo; + pSoldier->aiData.bOrders = CLOSEPATROL; + + // Find a gridno towards the opponent + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestOpponent, attack.ubAPCost, AI_ACTION_GET_CLOSER, 0); + // Try to find a cover spot near it + INT32 iCoverPercentBetter = 0; + INT32 spotNearTarget = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iCoverPercentBetter, pSoldier->aiData.usActionData); + if ( spotNearTarget != NOWHERE ) + { + // Found a spot near our destination that has better cover + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, spotNearTarget, attack.ubAPCost, AI_ACTION_GET_CLOSER, 0); + } + pSoldier->aiData.sPatrolGrid[0] = tgrd; + pSoldier->aiData.bOrders = oldOrders; + + if ( !TileIsOutOfBounds(pSoldier->aiData.usActionData) ) + { + pSoldier->aiData.bNextAction = AI_ACTION_FIRE_GUN; + pSoldier->aiData.usNextActionData = attack.sTarget; + pSoldier->aiData.bNextTargetLevel = attack.bTargetLevel; + + DebugAI(AI_MSG_INFO, pSoldier, String("try to get closer before shooting, move to %d", pSoldier->aiData.usActionData)); + return(AI_ACTION_GET_CLOSER); + } + } + + return(AI_ACTION_INVALID); +} + +//----------------------------------------------------------------- +// Boxer AI decision routines +//----------------------------------------------------------------- +INT8 DecideActionGreenBoxer(SOLDIERTYPE* pSoldier) +{ + DebugAI(AI_MSG_START, pSoldier, String("[Green Boxer]")); + LogDecideInfo(pSoldier); + + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // sevenfm: initialize data + pSoldier->bWeaponMode = WM_NORMAL; + + + if (gTacticalStatus.bBoxingState != NOT_BOXING) + { + if (BOXER(pSoldier)) + { + if (gTacticalStatus.bBoxingState == PRE_BOXING) + { + return(DecideActionBoxerEnteringRing(pSoldier)); + } + else + { + UINT16 usRoom; + UINT8 ubLoop; + + // boxer... but since in status green, it's time to leave the ring! + if (InARoom(pSoldier->sGridNo, &usRoom)) + { + if (usRoom == BOXING_RING) + { + for (ubLoop = 0; ubLoop < NUM_BOXERS; ++ubLoop) + { + if (pSoldier->ubID == gubBoxerID[ubLoop]) + { + // we should go back where we started + pSoldier->aiData.usActionData = gsBoxerGridNo[ubLoop]; + return(AI_ACTION_GET_CLOSER); + } + } + pSoldier->aiData.usActionData = FindClosestBoxingRingSpot(pSoldier, FALSE); + return(AI_ACTION_GET_CLOSER); + } + else + { + // done! + + // Flugente: only do this if we are not boxing. Otherwise this might interfere with boxing scripts, as they temporariyl set a PC under AI control (when leaaving the ring) + if (gTacticalStatus.bBoxingState == NOT_BOXING) + { + // WANNE: This should fix the bug if any merc are still under PC control. This could happen after boxing in SAN MONA. + for (SoldierID pTeamSoldier = gTacticalStatus.Team[gbPlayerNum].bFirstID; pTeamSoldier <= gTacticalStatus.Team[gbPlayerNum].bLastID; ++pTeamSoldier) + { + if (pTeamSoldier->flags.uiStatusFlags & SOLDIER_PCUNDERAICONTROL) + pTeamSoldier->flags.uiStatusFlags &= (~SOLDIER_PCUNDERAICONTROL); + + pTeamSoldier->DeleteBoxingFlag(); + } + } + + if (pSoldier->bTeam == gbPlayerNum || CountPeopleInBoxingRing() == 0) + { + TriggerEndOfBoxingRecord(pSoldier); + } + } + } + + return(AI_ACTION_ABSOLUTELY_NONE); + } + } + else if (PythSpacesAway(pSoldier->sGridNo, CENTER_OF_RING) <= MaxNormalDistanceVisible()) + { + UINT8 ubRingDir; + // face ring! + + ubRingDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, CENTER_OF_RING); + if (gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) + { + if (pSoldier->ubDirection != ubRingDir) + { + pSoldier->aiData.usActionData = ubRingDir; + return(AI_ACTION_CHANGE_FACING); + } + } + return(AI_ACTION_NONE); + } + } + + + //////////////////////////////////////////////////////////////////////////// + // NONE: + //////////////////////////////////////////////////////////////////////////// + + // by default, if everything else fails, just stands in place without turning + // for realtime, regular AI guys will use a standard wait set outside of here + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); +} + + +INT8 DecideActionBlackBoxer(SOLDIERTYPE* pSoldier) +{ + DebugAI(AI_MSG_START, pSoldier, String("[Black Boxer]")); + LogDecideInfo(pSoldier); + + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // sevenfm: initialize data + pSoldier->bWeaponMode = WM_NORMAL; + + // if we have absolutely no action points, we can't do a thing under BLACK! + if (pSoldier->bActionPoints <= 0) + { + pSoldier->aiData.usActionData = NOWHERE; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_NONE); + } + + // can this guy move to any of the neighbouring squares ? (sets TRUE/FALSE) + UINT8 ubCanMove = (pSoldier->bActionPoints >= MinPtsToMove(pSoldier)); + + // sevenfm: before deciding anything, stop cowering + if (SoldierAI(pSoldier) && + ubCanMove && + pSoldier->stats.bLife >= OKLIFE && + !pSoldier->bCollapsed && + !pSoldier->bBreathCollapsed && + pSoldier->IsCowering()) + { + return AI_ACTION_STOP_COWERING; + } + + + + if (gTacticalStatus.bBoxingState == PRE_BOXING) + { + return(DecideActionBoxerEnteringRing(pSoldier)); + } + else if (gTacticalStatus.bBoxingState == BOXING) + { + // for boxer, always use high morale + pSoldier->aiData.bAIMorale = MORALE_FEARLESS; + } + else //???? + { + return(AI_ACTION_NONE); + } + + + //////////////////////////////////////////////////////////////////////////// + // DETERMINE BEST ATTACK + //////////////////////////////////////////////////////////////////////////// + ATTACKTYPE BestShot, BestThrow, BestStab, BestAttack; + BestShot.ubPossible = FALSE; // by default, assume Shooting isn't possible + BestThrow.ubPossible = FALSE; // by default, assume Throwing isn't possible + BestStab.ubPossible = FALSE; // by default, assume Stabbing isn't possible + BestAttack.ubChanceToReallyHit = 0; + INT16 ubMinAPCost = 0; + + INT8 bWeaponIn = FindAIUsableObjClass(pSoldier, IC_PUNCH); + if (bWeaponIn == NO_SLOT) // if no punch-type weapon found, just calculate it with empty hands + { + bWeaponIn = FindEmptySlotWithin(pSoldier, HANDPOS, NUM_INV_SLOTS); + } + if (bWeaponIn != NO_SLOT) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "try to punch"); + BestStab.bWeaponIn = bWeaponIn; + // if it's in his holster, swap it into his hand temporarily + if (bWeaponIn != HANDPOS) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionblack: about to rearrange pocket before punch check"); + RearrangePocket(pSoldier, HANDPOS, bWeaponIn, TEMPORARILY); + } + + // get the minimum cost to attack with punch + ubMinAPCost = MinAPsToAttack(pSoldier, pSoldier->sLastTarget, DONTADDTURNCOST, 0, 0); + // if we can afford the minimum AP cost to punch + if (pSoldier->bActionPoints >= ubMinAPCost) + { + // then look around for a worthy target (which sets BestStab.ubPossible) + CalcBestStab(pSoldier, &BestStab, FALSE); + + if (BestStab.ubPossible) + { + // now we KNOW FOR SURE that we will do something (stab, at least) + NPCDoesAct(pSoldier); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "NPC decided to punch"); + } + + } + // if it was in his holster, swap it back into his holster for now + if (bWeaponIn != HANDPOS) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "about to rearrange pocket after punch check"); + RearrangePocket(pSoldier, HANDPOS, bWeaponIn, TEMPORARILY); + } + } + + + ////////////////////////////////////////////////////////////////////////// + // CHOOSE THE BEST TYPE OF ATTACK OUT OF THOSE FOUND TO BE POSSIBLE + ////////////////////////////////////////////////////////////////////////// + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "CHOOSE THE BEST TYPE OF ATTACK OUT OF THOSE FOUND TO BE POSSIBLE"); + BestAttack.iAttackValue = 0; + BOOLEAN fTryPunching = TRUE; + + // cautious boxer approach, reserve AP for two attacks (only if not attacking from the back) + if (BestStab.ubPossible && + SpacesAway(pSoldier->sGridNo, BestStab.sTarget) > 2 && + BestStab.ubOpponent != NOBODY && + AIDirection(pSoldier->sGridNo, BestStab.ubOpponent->sGridNo) != BestStab.ubOpponent->ubDirection && + AIDirection(pSoldier->sGridNo, BestStab.ubOpponent->sGridNo) != gOneCDirection[BestStab.ubOpponent->ubDirection] && + AIDirection(pSoldier->sGridNo, BestStab.ubOpponent->sGridNo) != gOneCCDirection[BestStab.ubOpponent->ubDirection] && + pSoldier->bInitialActionPoints >= 2 * MinAPsToAttack(pSoldier, pSoldier->sLastTarget, FALSE, 0, 0) + APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_WALK] && + pSoldier->bActionPoints < BestStab.ubAPCost + MinAPsToAttack(pSoldier, pSoldier->sLastTarget, FALSE, 0, 0)) + { + BestStab.ubPossible = FALSE; + fTryPunching = FALSE; + DebugAI(AI_MSG_INFO, pSoldier, String("boxer cannot reserve APs for second attack - disable stab attack")); + } + + // try to avoid frontal attack + if (BestStab.ubPossible && + SpacesAway(pSoldier->sGridNo, BestStab.sTarget) > 1 && + BestStab.ubOpponent != NOBODY && + BestStab.ubOpponent && + gAnimControl[BestStab.ubOpponent->usAnimState].ubEndHeight == ANIM_STAND && + BestStab.ubOpponent->bActionPoints > 0 && + Chance(EffectiveAgility(BestStab.ubOpponent, FALSE) * (100 + BestStab.ubOpponent->bBreath) * EffectiveWisdom(pSoldier) / (100 * 200))) + { + // find closest spot around opponent, avoid front direction + UINT8 ubMovementCost; + INT32 sTempGridNo; + UINT8 ubDirection; + INT32 sPathCost; + INT32 sBestSpot = NOWHERE; + INT32 sBestPathCost = 0; + + for (ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++) + { + sTempGridNo = NewGridNo(BestStab.sTarget, DirectionInc(ubDirection)); + + if (sTempGridNo != BestStab.sTarget) + { + ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][pSoldier->pathing.bLevel]; + + if (ubMovementCost < TRAVELCOST_BLOCKED && + NewOKDestination(pSoldier, sTempGridNo, FALSE, pSoldier->pathing.bLevel) && + AIDirection(BestStab.sTarget, sTempGridNo) != BestStab.ubOpponent->ubDirection) + { + sPathCost = PlotPath(pSoldier, sTempGridNo, FALSE, FALSE, FALSE, DetermineMovementMode(pSoldier, AI_ACTION_GET_CLOSER), pSoldier->bStealthMode, pSoldier->bReverse, 0); + if (TileIsOutOfBounds(sBestSpot) || sPathCost < sBestPathCost) + { + sBestSpot = sTempGridNo; + sBestPathCost = sPathCost; + } + } + } + } + + if (!TileIsOutOfBounds(sBestSpot) && + pSoldier->bActionPoints >= sPathCost + GetAPsToLook(pSoldier) + MinAPsToAttack(pSoldier, pSoldier->sLastTarget, FALSE, 0, 0)) + { + pSoldier->aiData.usActionData = sBestSpot; + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: get closer to opponent, avoid front direction")); + return(AI_ACTION_GET_CLOSER); + } + } + + + INT8 ubBestAttackAction = AI_ACTION_NONE; + if (fTryPunching) + { + // nothing (else) to attack with so let's try hand-to-hand + bWeaponIn = FindObj(pSoldier, NOTHING, HANDPOS, NUM_INV_SLOTS); + + if (bWeaponIn != NO_SLOT) + { + BestStab.bWeaponIn = bWeaponIn; + // if it's in his holster, swap it into his hand temporarily + if (bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("swap knife into hand")); + RearrangePocket(pSoldier, HANDPOS, bWeaponIn, TEMPORARILY); + } + + // get the minimum cost to attack by HTH + ubMinAPCost = MinAPsToAttack(pSoldier, pSoldier->sLastTarget, DONTADDTURNCOST, 0, 0); + + // if we can afford the minimum AP cost to use HTH combat + if (pSoldier->bActionPoints >= ubMinAPCost) + { + // then look around for a worthy target (which sets BestStab.ubPossible) + CalcBestStab(pSoldier, &BestStab, FALSE); + + if (BestStab.ubPossible) + { + // now we KNOW FOR SURE that we will do something (stab, at least) + NPCDoesAct(pSoldier); + ubBestAttackAction = AI_ACTION_KNIFE_MOVE; + DebugAI(AI_MSG_INFO, pSoldier, String("best action = move to stab, iAttackValue = %d", BestStab.iAttackValue)); + } + } + + // if it was in his holster, swap it back into his holster for now + if (bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("put knife away")); + RearrangePocket(pSoldier, HANDPOS, bWeaponIn, TEMPORARILY); + } + } + } + + // copy the information on the best action selected into BestAttack struct + DebugAI(AI_MSG_INFO, pSoldier, String("copy the information on the best action selected into BestAttack struct")); + switch (ubBestAttackAction) + { + case AI_ACTION_KNIFE_MOVE: + DebugAI(AI_MSG_INFO, pSoldier, String("Best attack - stab")); + memcpy(&BestAttack, &BestStab, sizeof(BestAttack)); + break; + + default: + // set to empty + DebugAI(AI_MSG_INFO, pSoldier, String("Best attack - no good attack")); + memset(&BestAttack, 0, sizeof(BestAttack)); + break; + } + + + ////////////////////////////////////////////////////////////////////////// + // PREPARE ATTACK + ////////////////////////////////////////////////////////////////////////// + + // if attack is still desirable (meaning it's also preferred to taking cover) + if (ubBestAttackAction != AI_ACTION_NONE) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Prepare attack]")); + + // default settings + //POSSIBLE STRUCTURE CHANGE PROBLEM, NOT CURRENTLY CHANGED. GOTTHARD 7/14/08 + pSoldier->aiData.bAimTime = BestAttack.ubAimTime; + pSoldier->bScopeMode = BestAttack.bScopeMode; + pSoldier->bDoBurst = 0; + + // HEADROCK HAM 3.6: bAimTime represents how MANY aiming levels are used, not how much APs they cost necessarily. + INT16 sActualAimAP = CalcAPCostForAiming(pSoldier, BestAttack.sTarget, (INT8)pSoldier->aiData.bAimTime); + + // SANDRO - chance to make aimed punch/stab for martial arts/melee + if (ubBestAttackAction == AI_ACTION_KNIFE_MOVE && gGameOptions.fNewTraitSystem) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Prepare knife attack]")); + + pSoldier->aiData.bAimTime = 0; + INT32 iChance = 0; + + if (Item[pSoldier->inv[BestAttack.bWeaponIn].usItem].usItemClass == IC_PUNCH) + { + if (gGameExternalOptions.fEnhancedCloseCombatSystem) + iChance += 30; + if (HAS_SKILL_TRAIT(pSoldier, MARTIAL_ARTS_NT)) + iChance += 30 * NUM_SKILL_TRAITS(pSoldier, MARTIAL_ARTS_NT); + + if ((INT32)PreRandom(100) <= iChance) + { + pSoldier->aiData.bAimTime = (gGameExternalOptions.fEnhancedCloseCombatSystem ? gSkillTraitValues.ubModifierForAPsAddedOnAimedPunches : 6); + } + } + else + { + if (gGameExternalOptions.fEnhancedCloseCombatSystem) + iChance += 30; + if (HAS_SKILL_TRAIT(pSoldier, MELEE_NT)) + iChance += 30; + + if ((INT32)PreRandom(100) <= iChance) + { + pSoldier->aiData.bAimTime = (gGameExternalOptions.fEnhancedCloseCombatSystem ? gSkillTraitValues.ubModifierForAPsAddedOnAimedBladedAttackes : 6); + } + } + } + + ////////////////////////////////////////////////////////////////////////// + // OTHERWISE, JUST GO AHEAD & ATTACK! + ////////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_TOPIC, pSoldier, String("Attack!")); + + // swap weapon to hand if necessary + if (BestAttack.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("swap weapon into hand from %d", BestAttack.bWeaponIn)); + RearrangePocket(pSoldier, HANDPOS, BestAttack.bWeaponIn, FOREVER); + } + + + DebugAI(AI_MSG_INFO, pSoldier, String("prepare attack at target %d level %d aim %d ap %d cth %d opponent %d", BestAttack.sTarget, BestAttack.bTargetLevel, BestAttack.ubAimTime, BestAttack.ubAPCost, BestAttack.ubChanceToReallyHit, BestAttack.ubOpponent)); + + { + pSoldier->aiData.usActionData = BestAttack.sTarget; + pSoldier->bTargetLevel = BestAttack.bTargetLevel; + return(ubBestAttackAction); + } + } + + + + ////////////////////////////////////////////////////////////////////// + // BOXER CLOSE IN ON OPPONENT + ////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Make boxer close if possible]")); + + SoldierID ubOpponentID; + INT32 sOpponentGridNo; + INT8 bOpponentLevel; + INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel, &ubOpponentID); + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: found closest opponent [%d] at %d", ubOpponentID, sClosestOpponent)); + + if (!TileIsOutOfBounds(sClosestOpponent) && ubOpponentID != NOBODY) + { + if (pSoldier->bActionPoints > 0) + { + if (SpacesAway(pSoldier->sGridNo, sClosestOpponent) > 1) + { + INT16 sReserveAP = GetAPsToLook(pSoldier) + 2 * MinAPsToAttack(pSoldier, pSoldier->sLastTarget, FALSE, 0, 0); + BOOLEAN fLimitOneStep = FALSE; + + if (pSoldier->bInitialActionPoints < sReserveAP + APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_WALK]) + { + sReserveAP = GetAPsToLook(pSoldier) + MinAPsToAttack(pSoldier, pSoldier->sLastTarget, FALSE, 0, 0); + } + + if (pSoldier->bInitialActionPoints < sReserveAP + APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_WALK] && + pSoldier->bInitialActionPoints >= GetAPsToLook(pSoldier) + MinAPsToAttack(pSoldier, pSoldier->sLastTarget, FALSE, 0, 0) && + SpacesAway(pSoldier->sGridNo, sClosestOpponent) > 2) + { + sReserveAP = GetAPsToLook(pSoldier) + 1; + fLimitOneStep = TRUE; + } + + // temporarily make boxer have orders of CLOSEPATROL rather than STATIONARY + // And make him patrol the ring, not his usual place + // so he has a good roaming range + INT32 tgrd = pSoldier->aiData.sPatrolGrid[0]; + pSoldier->aiData.sPatrolGrid[0] = pSoldier->sGridNo; + pSoldier->aiData.bOrders = CLOSEPATROL; + //pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards( pSoldier, sClosestOpponent, AI_ACTION_GET_CLOSER ); + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestOpponent, sReserveAP, AI_ACTION_GET_CLOSER, 0); + pSoldier->aiData.sPatrolGrid[0] = tgrd; + pSoldier->aiData.bOrders = STATIONARY; + + // decide to restore breath + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData) && + (pSoldier->bBreath < OKBREATH || + pSoldier->bBreath < pSoldier->bBreathMax && + pSoldier->bBreath < ubOpponentID->bBreath && + Chance((100 - pSoldier->bBreath) * (100 - pSoldier->bBreath) / (2 * 100 * 100)))) + { + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: restore breath")); + pSoldier->aiData.usActionData = NOWHERE; + } + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + // truncate path to 1 step + if (fLimitOneStep) + { + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: limit movement to one step")); + pSoldier->aiData.usActionData = pSoldier->sGridNo + DirectionInc((UINT8)pSoldier->pathing.usPathingData[0]); + pSoldier->pathing.sFinalDestination = pSoldier->aiData.usActionData; + } + + //pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: get closer to opponent")); + return(AI_ACTION_GET_CLOSER); + } + } + else if (pSoldier->bBreath < OKBREATH || pSoldier->bBreath < pSoldier->bBreathMax && + (pSoldier->bBreath < ubOpponentID->bBreath || !pSoldier->aiData.bLastAttackHit && pSoldier->TakenLargeHit())) + { + // maybe move away from opponent + UINT8 ubOpponentDir = AIDirection(pSoldier->sGridNo, sClosestOpponent); + INT32 sCheckGridNo = NewGridNo(pSoldier->sGridNo, DirectionInc(gOppositeDirection[ubOpponentDir])); + + // only use reverse movement if we are facing opponent + if (pSoldier->ubDirection == ubOpponentDir || + pSoldier->ubDirection == gOneCDirection[ubOpponentDir] || + pSoldier->ubDirection == gOneCCDirection[ubOpponentDir]) + { + pSoldier->bReverse = TRUE; + } + + if (!NewOKDestination(pSoldier, sCheckGridNo, FALSE, pSoldier->pathing.bLevel) || + PlotPath(pSoldier, sCheckGridNo, FALSE, FALSE, FALSE, DetermineMovementMode(pSoldier, AI_ACTION_TAKE_COVER), pSoldier->bStealthMode, pSoldier->bReverse, 0) > pSoldier->bActionPoints - 1) + { + //if (sPathcost > pSoldier->bActionPoints - (GetAPsToLook(pSoldier) + 1)) + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: bad destination or high path cost, cannot move away")); + sCheckGridNo = NOWHERE; + } + + // maybe try diagonal movement + if (TileIsOutOfBounds(sCheckGridNo) && Chance(50)) + { + sCheckGridNo = NewGridNo(pSoldier->sGridNo, DirectionInc(gOneCDirection[gOppositeDirection[ubOpponentDir]])); + if (!NewOKDestination(pSoldier, sCheckGridNo, FALSE, pSoldier->pathing.bLevel) || + PlotPath(pSoldier, sCheckGridNo, FALSE, FALSE, FALSE, DetermineMovementMode(pSoldier, AI_ACTION_TAKE_COVER), pSoldier->bStealthMode, pSoldier->bReverse, 0) > pSoldier->bActionPoints - 1) + { + //if (sPathcost > pSoldier->bActionPoints - (GetAPsToLook(pSoldier) + 1)) + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: bad destination or high path cost, cannot move away")); + sCheckGridNo = NOWHERE; + } + } + if (TileIsOutOfBounds(sCheckGridNo) && Chance(50)) + { + sCheckGridNo = NewGridNo(pSoldier->sGridNo, DirectionInc(gOneCCDirection[gOppositeDirection[ubOpponentDir]])); + if (!NewOKDestination(pSoldier, sCheckGridNo, FALSE, pSoldier->pathing.bLevel) || + PlotPath(pSoldier, sCheckGridNo, FALSE, FALSE, FALSE, DetermineMovementMode(pSoldier, AI_ACTION_TAKE_COVER), pSoldier->bStealthMode, pSoldier->bReverse, 0) > pSoldier->bActionPoints - 1) + { + //if (sPathcost > pSoldier->bActionPoints - (GetAPsToLook(pSoldier) + 1)) + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: bad destination or high path cost, cannot move away")); + sCheckGridNo = NOWHERE; + } + } + + if (!TileIsOutOfBounds(sCheckGridNo)) + { + pSoldier->aiData.usActionData = sCheckGridNo; + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: get away from opponent")); + return(AI_ACTION_TAKE_COVER); + } + pSoldier->bReverse = FALSE; + } + } + + UINT8 ubOpponentDir = AIDirection(pSoldier->sGridNo, sClosestOpponent); + + // possibly turn to closest opponent + if (pSoldier->ubDirection != ubOpponentDir && + pSoldier->InternalIsValidStance(ubOpponentDir, gAnimControl[pSoldier->usAnimState].ubEndHeight) && + pSoldier->bActionPoints >= GetAPsToLook(pSoldier)) + { + pSoldier->aiData.usActionData = ubOpponentDir; + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: turn to closest opponent")); + return(AI_ACTION_CHANGE_FACING); + } + } + + // otherwise do nothing + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: nothing to do")); + return(AI_ACTION_NONE); +} + + +//----------------------------------------------------------------- +// Refactored AI decision routines +//----------------------------------------------------------------- +INT8 DecideActionGreenCivilian(SOLDIERTYPE* pSoldier) +{ + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen, orders = %d", pSoldier->aiData.bOrders)); + + DebugAI(AI_MSG_START, pSoldier, String("[Green Civilian]")); + LogDecideInfo(pSoldier); + + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // sevenfm: initialize data + pSoldier->bWeaponMode = WM_NORMAL; + + BOOLEAN fCivilianOrMilitia = PTR_CIV_OR_MILITIA; + + gubNPCPathCount = 0; + + + if (gTacticalStatus.bBoxingState != NOT_BOXING) + { + if (PythSpacesAway(pSoldier->sGridNo, CENTER_OF_RING) <= MaxNormalDistanceVisible()) + { + UINT8 ubRingDir; + // face ring! + + ubRingDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, CENTER_OF_RING); + if (gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) + { + if (pSoldier->ubDirection != ubRingDir) + { + pSoldier->aiData.usActionData = ubRingDir; + return(AI_ACTION_CHANGE_FACING); + } + } + return(AI_ACTION_NONE); + } + } + + + INT8 bInWater, bInDeepWater, bInGas; + bInWater = Water(pSoldier->sGridNo, pSoldier->pathing.bLevel); + bInDeepWater = DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel); + bInGas = InGasOrSmoke(pSoldier, pSoldier->sGridNo); + + // if real-time, and not in the way, do nothing 90% of the time (for GUARDS!) + // unless in water (could've started there), then we better swim to shore! + // special stuff for civs + if (pSoldier->flags.uiStatusFlags & SOLDIER_COWERING) + { + // everything's peaceful again, stop cowering!! + pSoldier->aiData.usActionData = ANIM_STAND; + return(AI_ACTION_STOP_COWERING); + } + + if (!gfTurnBasedAI) + { + // ****************** + // REAL TIME NPC CODE + // ****************** + if (pSoldier->aiData.fAIFlags & AI_CHECK_SCHEDULE) + { + pSoldier->aiData.bAction = DecideActionSchedule(pSoldier); + if (pSoldier->aiData.bAction != AI_ACTION_NONE) + { + return(pSoldier->aiData.bAction); + } + } + + if (pSoldier->ubProfile != NO_PROFILE || pSoldier->IsAssassin()) + { + if (pSoldier->ubProfile != NO_PROFILE) + pSoldier->aiData.bAction = DecideActionNamedNPC(pSoldier); + else + { + INT32 sDesiredMercDist; + INT32 sDesiredMercLoc = ClosestUnDisguisedPC(pSoldier, &sDesiredMercDist); + + if (!TileIsOutOfBounds(sDesiredMercLoc)) + { + if (sDesiredMercDist <= NPC_TALK_RADIUS * 2) + { + AddToShouldBecomeHostileOrSayQuoteList(pSoldier->ubID); + // now wait a bit! + pSoldier->aiData.usActionData = 5000; + pSoldier->aiData.bAction = AI_ACTION_WAIT; + } + else + { + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sDesiredMercLoc, AI_ACTION_APPROACH_MERC); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + pSoldier->aiData.bAction = AI_ACTION_APPROACH_MERC; + } + } + } + } + + if (pSoldier->aiData.bAction != AI_ACTION_NONE) + { + return(pSoldier->aiData.bAction); + } + // can we act again? not for a minute since we were last spoken to/triggered a record + if (pSoldier->uiTimeSinceLastSpoke && (GetJA2Clock() < pSoldier->uiTimeSinceLastSpoke + 60000)) + { + return(AI_ACTION_NONE); + } + // turn off counter so we don't check it again + pSoldier->uiTimeSinceLastSpoke = 0; + } + } + + // if not in the way, do nothing most of the time + // unless in water (could've started there), then we better swim to shore! + if (!(bInDeepWater) && PreRandom(5)) + { + // don't do nuttin! + return(AI_ACTION_NONE); + } + + + //////////////////////////////////////////////////////////////////////////// + // POINT PATROL: move towards next point unless getting a bit winded + //////////////////////////////////////////////////////////////////////////// + + // this takes priority over water/gas checks, so that point patrol WILL work + // from island to island, and through gas covered areas, too + if ((pSoldier->aiData.bOrders == POINTPATROL) && (pSoldier->bBreath >= 75)) + { + if (PointPatrolAI(pSoldier)) + { + if (!gfTurnBasedAI) + { + // wait after this... + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = RealtimeDelay(pSoldier); + } + return(AI_ACTION_POINT_PATROL); + } + else + { + // Reset path count to avoid dedlok + gubNPCPathCount = 0; + } + } + + if ((pSoldier->aiData.bOrders == RNDPTPATROL) && (pSoldier->bBreath >= 75)) + { + if (RandomPointPatrolAI(pSoldier)) + { + if (!gfTurnBasedAI) + { + // wait after this... + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = RealtimeDelay(pSoldier); + } + return(AI_ACTION_POINT_PATROL); + } + else + { + // Reset path count to avoid dedlok + gubNPCPathCount = 0; + } + + } + + //////////////////////////////////////////////////////////////////////////// + // WHEN LEFT IN WATER OR GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND + //////////////////////////////////////////////////////////////////////////// + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: get out of water and gas")); + + if (bInDeepWater || bInGas || FindBombNearby(pSoldier, pSoldier->sGridNo, BOMB_DETECTION_RANGE) || RedSmokeDanger(pSoldier->sGridNo, pSoldier->pathing.bLevel)) + { + pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_LEAVE_WATER_GAS); + } + } + + + + //////////////////////////////////////////////////////////////////////// + // REST IF RUNNING OUT OF BREATH + //////////////////////////////////////////////////////////////////////// + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: rest if running out of breath")); + // if our breath is running a bit low, and we're not in the way or in water + if ((pSoldier->bBreath < 75) && !bInWater) + { + // take a breather for gods sake! + // for realtime, AI will use a standard wait set outside of here + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); + } + + + //////////////////////////////////////////////////////////////////////////// + // RANDOM PATROL: determine % chance to start a new patrol route + //////////////////////////////////////////////////////////////////////////// + INT32 iChance, iSneaky = 10; + if (!gubNPCPathCount) // try to limit pathing in Green AI + { + + iChance = 25 + pSoldier->aiData.bBypassToGreen; + + // set base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += -20; break; + case ONGUARD: iChance += -15; break; + case ONCALL: break; + case CLOSEPATROL: iChance += +15; break; + case RNDPTPATROL: + case POINTPATROL: iChance = 0; break; + case FARPATROL: iChance += +25; break; + case SEEKENEMY: iChance += -10; break; + case SNIPER: iChance += -10; break; + } + + // modify chance of patrol (and whether it's a sneaky one) by attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += -10; break; + case BRAVESOLO: iChance += 5; break; + case BRAVEAID: break; + case CUNNINGSOLO: iChance += 5; iSneaky += 10; break; + case CUNNINGAID: iSneaky += 5; break; + case AGGRESSIVE: iChance += 10; iSneaky += -5; break; + case ATTACKSLAYONLY: iChance += 10; iSneaky += -5; break; + } + + // reduce chance for any injury, less likely to wander around when hurt + iChance -= (pSoldier->stats.bLifeMax - pSoldier->stats.bLife); + + // reduce chance if breath is down, less likely to wander around when tired + iChance -= (100 - pSoldier->bBreath); + + + // if we're in water with land miles (> 25 tiles) away, + // OR if we roll under the chance calculated + if (bInWater || ((INT16)PreRandom(100) < iChance)) + { + pSoldier->aiData.usActionData = RandDestWithinRange(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, pSoldier->aiData.usActionData, AI_ACTION_RANDOM_PATROL); + } + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - RANDOM PATROL to grid %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + if (!gfTurnBasedAI) + { + // wait after this... + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = RealtimeDelay(pSoldier); + } + return(AI_ACTION_RANDOM_PATROL); + } + } + } + + if (!gubNPCPathCount) // try to limit pathing in Green AI + { + //////////////////////////////////////////////////////////////////////////// + // SEEK FRIEND: determine %chance for man to pay a friendly visit + //////////////////////////////////////////////////////////////////////////// + + iChance = 25 + pSoldier->aiData.bBypassToGreen; + + // set base chance and maximum seeking distance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += -20; break; + case ONGUARD: iChance += -15; break; + case ONCALL: break; + case CLOSEPATROL: iChance += +10; break; + case RNDPTPATROL: + case POINTPATROL: iChance = -10; break; + case FARPATROL: iChance += +20; break; + case SEEKENEMY: iChance += -10; break; + case SNIPER: iChance += -10; break; + } + + // modify for attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: break; + case BRAVESOLO: iChance /= 2; break; // loners + case BRAVEAID: iChance += 10; break; // friendly + case CUNNINGSOLO: iChance /= 2; break; // loners + case CUNNINGAID: iChance += 10; break; // friendly + case AGGRESSIVE: break; + case ATTACKSLAYONLY: break; + } + + // reduce chance for any injury, less likely to wander around when hurt + iChance -= (pSoldier->stats.bLifeMax - pSoldier->stats.bLife); + + // reduce chance if breath is down + iChance -= (100 - pSoldier->bBreath); // very likely to wait when exhausted + + + if ((INT16)PreRandom(100) < iChance) + { + if (RandomFriendWithin(pSoldier)) + { + if (pSoldier->aiData.usActionData == GoAsFarAsPossibleTowards(pSoldier, pSoldier->aiData.usActionData, AI_ACTION_SEEK_FRIEND)) + { + if (fCivilianOrMilitia && !gfTurnBasedAI) + { + // pause at the end of the walk! + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = (UINT16)REALTIME_CIV_AI_DELAY; + } + + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + + + + //////////////////////////////////////////////////////////////////////////// + // LOOK AROUND: determine %chance for man to turn in place + //////////////////////////////////////////////////////////////////////////// + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Soldier deciding to turn")); + if (!gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) + { + // avoid 2 consecutive random turns in a row + if (pSoldier->aiData.bLastAction != AI_ACTION_CHANGE_FACING) + { + iChance = 25 + pSoldier->aiData.bBypassToGreen; + + // set base chance according to orders + if (pSoldier->aiData.bOrders == STATIONARY || pSoldier->aiData.bOrders == SNIPER) + iChance += 25; + + if (pSoldier->aiData.bOrders == ONGUARD) + iChance += 20; + + if (pSoldier->aiData.bAttitude == DEFENSIVE) + iChance += 25; + + if (pSoldier->aiData.bOrders == SNIPER && pSoldier->pathing.bLevel == 1) + iChance += 35; + + if (WeaponReady(pSoldier)) // SANDRO - if readied weapon, make him more likely to turn around + iChance += 30; + + if ((INT16)PreRandom(100) < iChance) + { + // roll random directions (stored in actionData) until different from current + do + { + // if man has a LEGAL dominant facing, and isn't facing it, he will turn + // back towards that facing 50% of the time here (normally just enemies) + if ((pSoldier->aiData.bDominantDir >= 0) && (pSoldier->aiData.bDominantDir <= 8) && + (pSoldier->ubDirection != pSoldier->aiData.bDominantDir) && PreRandom(2) && pSoldier->aiData.bOrders != SNIPER) + { + pSoldier->aiData.usActionData = pSoldier->aiData.bDominantDir; + } + else + { + INT32 iNoiseValue; + BOOLEAN fClimb; + BOOLEAN fReachable; + INT32 sNoiseGridNo = MostImportantNoiseHeard(pSoldier, &iNoiseValue, &fClimb, &fReachable); + UINT8 ubNoiseDir; + + if (TileIsOutOfBounds(sNoiseGridNo) || + (ubNoiseDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sNoiseGridNo)) == pSoldier->ubDirection) + + { + pSoldier->aiData.usActionData = PreRandom(8); + } + else + { + pSoldier->aiData.usActionData = ubNoiseDir; + } + } + } while (pSoldier->aiData.usActionData == pSoldier->ubDirection); + + +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - TURNS to face direction %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Trying to turn - checking stance validity, sniper = %d", pSoldier->sniper)); + if (pSoldier->InternalIsValidStance((INT8)pSoldier->aiData.usActionData, gAnimControl[pSoldier->usAnimState].ubEndHeight)) + { + + if (!gfTurnBasedAI) + { + // wait after this... + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = RealtimeDelay(pSoldier); + } + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Soldier is turning")); + return(AI_ACTION_CHANGE_FACING); + } + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // NONE: + //////////////////////////////////////////////////////////////////////////// + + // by default, if everything else fails, just stands in place without turning + // for realtime, regular AI guys will use a standard wait set outside of here + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); +} + +INT8 DecideActionYellowCivilian(SOLDIERTYPE* pSoldier) +{ + DebugAI(AI_MSG_START, pSoldier, String("[Yellow Civilian]")); + LogDecideInfo(pSoldier); + + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // sevenfm: initialize data + pSoldier->bWeaponMode = WM_NORMAL; + + + if (pSoldier->flags.uiStatusFlags & SOLDIER_COWERING) + { + // everything's peaceful again, stop cowering!! + pSoldier->aiData.usActionData = ANIM_STAND; + return(AI_ACTION_STOP_COWERING); + } + if (!gfTurnBasedAI) + { + // ****************** + // REAL TIME NPC CODE + // ****************** + if (pSoldier->ubProfile != NO_PROFILE || pSoldier->IsAssassin()) + { + if (pSoldier->ubProfile != NO_PROFILE) + pSoldier->aiData.bAction = DecideActionNamedNPC(pSoldier); + else + { + INT32 sDesiredMercDist; + INT32 sDesiredMercLoc = ClosestUnDisguisedPC(pSoldier, &sDesiredMercDist); + + // Flugente: if this guy is disguised, do not consider him + + if (!TileIsOutOfBounds(sDesiredMercLoc)) + { + if (sDesiredMercDist <= NPC_TALK_RADIUS * 2) + { + AddToShouldBecomeHostileOrSayQuoteList(pSoldier->ubID); + // now wait a bit! + pSoldier->aiData.usActionData = 5000; + pSoldier->aiData.bAction = AI_ACTION_WAIT; + } + else + { + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sDesiredMercLoc, AI_ACTION_APPROACH_MERC); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + pSoldier->aiData.bAction = AI_ACTION_APPROACH_MERC; + } + } + } + } + + if (pSoldier->aiData.bAction != AI_ACTION_NONE) + { + return(pSoldier->aiData.bAction); + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // WHEN IN GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND + //////////////////////////////////////////////////////////////////////////// + if (InGas(pSoldier, pSoldier->sGridNo) || DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel) || FindBombNearby(pSoldier, pSoldier->sGridNo, BOMB_DETECTION_RANGE)) + { + pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_LEAVE_WATER_GAS); + } + } + + // determine the most important noise heard, and its relative value + INT32 iNoiseValue; + BOOLEAN fClimb; + BOOLEAN fReachable; + INT32 sNoiseGridNo = MostImportantNoiseHeard(pSoldier, &iNoiseValue, &fClimb, &fReachable); + + if (TileIsOutOfBounds(sNoiseGridNo)) + { + // then we have no business being under YELLOW status any more! + return(AI_ACTION_NONE); + } + + + //////////////////////////////////////////////////////////////////////////// + // LOOK AROUND TOWARD NOISE: determine %chance for man to turn towards noise + //////////////////////////////////////////////////////////////////////////// + + // determine direction from this soldier in which the noise lies + UINT8 ubNoiseDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sNoiseGridNo); + + // if soldier is not already facing in that direction, + // and the noise source is close enough that it could possibly be seen + if (!gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) + { + if ((pSoldier->ubDirection != ubNoiseDir) && PythSpacesAway(pSoldier->sGridNo, sNoiseGridNo) <= pSoldier->GetMaxDistanceVisible(sNoiseGridNo)) + { + INT32 iChance; + // set base chance according to orders + if ((pSoldier->aiData.bOrders == STATIONARY) || (pSoldier->aiData.bOrders == ONGUARD)) + iChance = 50; + else // all other orders + iChance = 25; + + if (pSoldier->aiData.bAttitude == DEFENSIVE) + iChance += 15; + + + if ((INT16)PreRandom(100) < iChance && pSoldier->InternalIsValidStance(ubNoiseDir, gAnimControl[pSoldier->usAnimState].ubEndHeight)) + { + pSoldier->aiData.usActionData = ubNoiseDir; + if (pSoldier->aiData.bOrders == SNIPER && + (pSoldier->bBreath > 25 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 30) && + !WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) + { + if (!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, READY_RIFLE_CROUCH) <= pSoldier->bActionPoints) + { + pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; + } + } + //////////////////////////////////////////////////////////////////////////// + // SANDRO - allow regular soldiers to raise scoped weapons to see farther away too + if (IsScoped(&pSoldier->inv[HANDPOS])) + { + if (!WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION && + (pSoldier->bBreath > 25 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 30)) + { + if (!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, PickSoldierReadyAnimation(pSoldier, FALSE, FALSE)) <= pSoldier->bActionPoints) + { + if (Random(100) < 35) + { + pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; + } + } + } + } + //////////////////////////////////////////////////////////////////////////// + + return(AI_ACTION_CHANGE_FACING); + } + } + } + + + //////////////////////////////////////////////////////////////////////// + // REST IF RUNNING OUT OF BREATH + //////////////////////////////////////////////////////////////////////// + + // if our breath is running a bit low, and we're not in water + if ((pSoldier->bBreath < 25) && !pSoldier->MercInWater()) + { + // take a breather for gods sake! + pSoldier->aiData.usActionData = NOWHERE; + + // is it a heavy gun? And we have energy cost for shooting enabled? + if (WeaponReady(pSoldier) && GetBPCostPer10APsForGunHolding(pSoldier) > 0) + { + // unready + return(AI_ACTION_LOWER_GUN); + } + + return(AI_ACTION_NONE); + } + + + // However, civilians with no profile (and likely no weapons) do not need to be seeking out noises. Most don't + // even have the body type for it (can't climb or jump). + // ADB: Eldin is the only neutral civilian who should be seeking out noises. As the museum curator, he can be + // available to talk to. As the night watchman, he needs to look for thieves. + bool onCivTeam = (pSoldier->bTeam == CIV_TEAM); + bool isNamedCiv = (pSoldier->ubProfile != NO_PROFILE); + bool isEldin = (pSoldier->ubProfile == ELDIN);//logically flipped from the original, isNotEldin == false is confusing + // For purpose of seeking noise, cowardly civs are neutral, even if attacked by your thugs + bool isNeutral = pSoldier->aiData.bNeutral || pSoldier->flags.uiStatusFlags & SOLDIER_COWERING; + if ( + (onCivTeam == false) || //true #1 + (onCivTeam == true && isNamedCiv == true && isNeutral == false) || //true #2 + (onCivTeam == true && isEldin == true)//true #3 + ) + { + // IF WE ARE MILITIA/CIV IN REALTIME, CLOSE TO NOISE, AND CAN SEE THE SPOT WHERE THE NOISE CAME FROM, FORGET IT + if (fReachable && !fClimb && !gfTurnBasedAI && (pSoldier->bTeam == MILITIA_TEAM || pSoldier->bTeam == CIV_TEAM) && PythSpacesAway(pSoldier->sGridNo, sNoiseGridNo) < 5) + { + if (SoldierTo3DLocationLineOfSightTest(pSoldier, sNoiseGridNo, pSoldier->pathing.bLevel, 0, TRUE, 6)) + { + // set reachable to false so we don't investigate + fReachable = FALSE; + // forget about noise + pSoldier->aiData.sNoiseGridno = NOWHERE; + pSoldier->aiData.ubNoiseVolume = 0; + } + } + + //////////////////////////////////////////////////////////////////////////// + // SEEK NOISE + //////////////////////////////////////////////////////////////////////////// + + if (fReachable) + { + // remember that noise value is negative, and closer to 0 => more important! + INT32 iChance = 95 + (iNoiseValue / 3); + INT32 iSneaky = 30; + + // increase + + // set base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += -20; break; + case ONGUARD: iChance += -15; break; + case ONCALL: break; + case CLOSEPATROL: iChance += -10; break; + case RNDPTPATROL: + case POINTPATROL: break; + case FARPATROL: iChance += 10; break; + case SEEKENEMY: iChance += 25; break; + case SNIPER: iChance += -10; break; + } + + // modify chance of patrol (and whether it's a sneaky one) by attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += -10; iSneaky += 15; break; + case BRAVESOLO: iChance += 10; break; + case BRAVEAID: iChance += 5; break; + case CUNNINGSOLO: iChance += 5; iSneaky += 30; break; + case CUNNINGAID: iSneaky += 30; break; + case AGGRESSIVE: iChance += 20; iSneaky += -10; break; + case ATTACKSLAYONLY: iChance += 20; iSneaky += -10; break; + } + + + // reduce chance if breath is down, less likely to wander around when tired + iChance -= (100 - pSoldier->bBreath); + + //Madd: make militia less likely to go running headlong into trouble + if (pSoldier->bTeam == MILITIA_TEAM) + iChance -= 30; + + if ((INT16)PreRandom(100) < iChance) + { + + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sNoiseGridNo, AI_ACTION_SEEK_NOISE); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + if (!ENEMYROBOT(pSoldier) && fClimb)//&& pSoldier->aiData.usActionData == sNoiseGridNo) + { + // need to climb AND have enough APs to get there this turn + BOOLEAN fUp = TRUE; + if (pSoldier->pathing.bLevel > 0) + fUp = FALSE; + + if (!fUp) + DebugMsg(TOPIC_JA2AI, DBG_LEVEL_3, String("Soldier %d, is climbing down", pSoldier->ubID)); + + // 0verhaul: the Closest Noise call returns the location of a climb. So 1) it's not necessary to + // ask if we can climb from here. And 2) It's not necessary to look for the climb point. We already + // have it. +// if ( CanClimbFromHere ( pSoldier, fUp ) ) + if (pSoldier->sGridNo == sNoiseGridNo) + { + if (IsActionAffordable(pSoldier) && pSoldier->bActionPoints >= (APBPConstants[AP_CLIMBROOF] + MinAPsToAttack(pSoldier, sNoiseGridNo, ADDTURNCOST, 0))) + { + return(AI_ACTION_CLIMB_ROOF); + } + } + else + { + // pSoldier->aiData.usActionData = FindClosestClimbPoint(pSoldier, pSoldier->sGridNo , sNoiseGridNo , fUp ); + pSoldier->aiData.usActionData = sNoiseGridNo; + //if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_MOVE_TO_CLIMB); + } + } + } + + // possibly start YELLOW flanking + if (gGameExternalOptions.fAIYellowFlanking && + (pSoldier->aiData.bAttitude == CUNNINGAID || pSoldier->aiData.bAttitude == CUNNINGSOLO) && + pSoldier->bTeam == ENEMY_TEAM && + (CountFriendsInDirection(pSoldier, sNoiseGridNo) > 0 || NightTime()) && + (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); + if (dist > MIN_FLANK_DIST_YELLOW && dist < MAX_FLANK_DIST_YELLOW) + { + INT16 rdm = Random(6); + + switch (rdm) + { + case 1: + case 2: + case 3: + if (pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT) + action = AI_ACTION_FLANK_LEFT; + break; + default: + if (pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT) + action = AI_ACTION_FLANK_RIGHT; + break; + } + } + else + return AI_ACTION_SEEK_NOISE; + + pSoldier->aiData.usActionData = FindFlankingSpot(pSoldier, sNoiseGridNo, action); + + if (TileIsOutOfBounds(pSoldier->aiData.usActionData) || pSoldier->numFlanks >= MAX_FLANKS_YELLOW) + { + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sNoiseGridNo, AI_ACTION_SEEK_NOISE); + //pSoldier->numFlanks = 0; + return(AI_ACTION_SEEK_NOISE); + } + else + { + if (action == AI_ACTION_FLANK_LEFT) + pSoldier->flags.lastFlankLeft = TRUE; + else + pSoldier->flags.lastFlankLeft = FALSE; + + if (pSoldier->lastFlankSpot != sNoiseGridNo) + pSoldier->numFlanks = 0; + + pSoldier->origDir = GetDirectionFromGridNo(sNoiseGridNo, pSoldier); + pSoldier->lastFlankSpot = sNoiseGridNo; + pSoldier->numFlanks++; + + // sevenfm: change orders CLOSEPATROL -> FARPATROL + if (pSoldier->aiData.bOrders == CLOSEPATROL) + { + pSoldier->aiData.bOrders = FARPATROL; + } + + return(action); + } + } + else + { + return(AI_ACTION_SEEK_NOISE); + } + + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // SEEK FRIEND WHO LAST RADIOED IN TO REPORT NOISE + //////////////////////////////////////////////////////////////////////////// + + INT32 sClosestFriend = ClosestReachableFriendInTrouble(pSoldier, &fClimb); + + // if there is a friend alive & reachable who last radioed in + if (!TileIsOutOfBounds(sClosestFriend)) + { + // there a chance enemy soldier choose to go "help" his friend + INT32 iChance = 50 - SpacesAway(pSoldier->sGridNo, sClosestFriend); + INT32 iSneaky = 10; + + // set base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += -20; break; + case ONGUARD: iChance += -15; break; + case ONCALL: iChance += 20; break; + case CLOSEPATROL: iChance += -10; break; + case RNDPTPATROL: + case POINTPATROL: iChance += -10; break; + case FARPATROL: break; + case SEEKENEMY: iChance += 10; break; + case SNIPER: iChance += -10; break; + } + + // modify chance of patrol (and whether it's a sneaky one) by attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += -10; iSneaky += 15; break; + case BRAVESOLO: break; + case BRAVEAID: iChance += 20; iSneaky += -10; break; + case CUNNINGSOLO: iSneaky += 30; break; + case CUNNINGAID: iChance += 20; iSneaky += 20; break; + case AGGRESSIVE: iChance += -20; iSneaky += -20; break; + case ATTACKSLAYONLY: iChance += -20; iSneaky += -20; break; + } + + // reduce chance if breath is down, less likely to wander around when tired + iChance -= (100 - pSoldier->bBreath); + + if ((INT16)PreRandom(100) < iChance) + { + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sClosestFriend, AI_ACTION_SEEK_FRIEND); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + if (!ENEMYROBOT(pSoldier) && fClimb)//&& pSoldier->aiData.usActionData == sClosestFriend) + { + // need to climb AND have enough APs to get there this turn + BOOLEAN fUp = TRUE; + if (pSoldier->pathing.bLevel > 0) + fUp = FALSE; + + if (!fUp) + DebugMsg(TOPIC_JA2AI, DBG_LEVEL_3, String("Soldier %d is climbing down", pSoldier->ubID)); + + // 0verhaul: Closest Friend call also returns the climb point if climbing is necessary. So don't + // climb the wrong building and don't search again + //if ( CanClimbFromHere ( pSoldier, fUp ) ) + if (pSoldier->sGridNo == sClosestFriend) + { + if (IsActionAffordable(pSoldier)) + { + return(AI_ACTION_CLIMB_ROOF); + } + } + else + { + //pSoldier->aiData.usActionData = FindClosestClimbPoint(pSoldier, pSoldier->sGridNo , sClosestFriend , fUp ); + pSoldier->aiData.usActionData = sClosestFriend; + //if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_MOVE_TO_CLIMB); + } + } + } + + return(AI_ACTION_SEEK_FRIEND); + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // TAKE BEST NEARBY COVER FROM THE NOISE GENERATING GRIDNO + //////////////////////////////////////////////////////////////////////////// + + if (!SkipCoverCheck) // && gfTurnBasedAI) // only do in turnbased + { + // remember that noise value is negative, and closer to 0 => more important! + INT32 iChance = 25; + INT32 iSneaky = 30; + + // set base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += 20; break; + case ONGUARD: iChance += 15; break; + case ONCALL: break; + case CLOSEPATROL: iChance += 10; break; + case RNDPTPATROL: + case POINTPATROL: break; + case FARPATROL: iChance += -5; break; + case SEEKENEMY: iChance += -20; break; + case SNIPER: iChance += 20; break; + } + + // modify chance (and whether it's sneaky) by attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += 10; iSneaky += 15; break; + case BRAVESOLO: iChance += -15; iSneaky += -20; break; + case BRAVEAID: iChance += -20; iSneaky += -20; break; + case CUNNINGSOLO: iChance += 20; iSneaky += 30; break; + case CUNNINGAID: iChance += 15; iSneaky += 30; break; + case AGGRESSIVE: iChance += -10; iSneaky += -10; break; + case ATTACKSLAYONLY: iChance += -10; iSneaky += -10; break; + } + + + //Madd: make militia more likely to take cover + if (pSoldier->bTeam == MILITIA_TEAM) + iChance += 20; + + // reduce chance if breath is down, less likely to wander around when tired + iChance -= (100 - pSoldier->bBreath); + + if ((INT16)PreRandom(100) < iChance) + { + INT32 iDummy; + pSoldier->aiData.bAIMorale = CalcMorale(pSoldier); + pSoldier->aiData.usActionData = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iDummy); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - TAKING COVER at grid %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + return(AI_ACTION_TAKE_COVER); + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // SWITCH TO GREEN: determine if soldier acts as if nothing at all was wrong + //////////////////////////////////////////////////////////////////////////// + if ((INT16)PreRandom(100) < 50) + { + // Skip YELLOW until new situation, 15% extra chance to do GREEN actions + pSoldier->aiData.bBypassToGreen = 15; + return(DecideActionGreenCivilian(pSoldier)); + } + + + //////////////////////////////////////////////////////////////////////////// + // DO NOTHING: Not enough points left to move, so save them for next turn + //////////////////////////////////////////////////////////////////////////// + // by default, if everything else fails, just stands in place without turning + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); +} + +INT8 DecideActionRedCivilian(SOLDIERTYPE* pSoldier) +{ + DebugAI(AI_MSG_START, pSoldier, String("[Red Civilian]"), gLogDecideActionRed); + LogDecideInfo(pSoldier, gLogDecideActionRed); + + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // sevenfm: initialize data + pSoldier->bWeaponMode = WM_NORMAL; + + // if we have absolutely no action points, we can't do a thing under RED! + if (pSoldier->bActionPoints <= 0) //Action points can be negative + { + pSoldier->aiData.usActionData = NOWHERE; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_NONE); + } + + BOOLEAN fProneSightCover = ProneSightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE); + DebugAI(AI_MSG_INFO, pSoldier, String("prone sight cover %d", fProneSightCover), gLogDecideActionRed); + + BOOLEAN fDangerousSpot = FALSE; + if (!fProneSightCover || pSoldier->aiData.bUnderFire) + { + fDangerousSpot = TRUE; + } + + // can this guy move to any of the neighbouring squares ? (sets TRUE/FALSE) + UINT8 ubCanMove = (pSoldier->bActionPoints >= MinPtsToMove(pSoldier)); + + // determine if we happen to be in water (in which case we're in BIG trouble!) + INT8 bInWater = Water(pSoldier->sGridNo, pSoldier->pathing.bLevel); + INT8 bInDeepWater = DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel); + + //////////////////////////////////////////////////////////////////////////// + // WHEN LEFT IN GAS, WEAR GAS MASK IF AVAILABLE AND NOT WORN + //////////////////////////////////////////////////////////////////////////// + INT8 bInGas = DecideActionWearGasmask(pSoldier); + + //////////////////////////////////////////////////////////////////////////// + // WHEN IN GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND + //////////////////////////////////////////////////////////////////////////// + // when in deep water, move to closest opponent + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide action if stuck in water or gas]"), gLogDecideActionRed); + if (ubCanMove && (bInGas || bInDeepWater || FindBombNearby(pSoldier, pSoldier->sGridNo, BOMB_DETECTION_RANGE) || RedSmokeDanger(pSoldier->sGridNo, pSoldier->pathing.bLevel))) + { + pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Leave for nearest (ungassed) land"), gLogDecideActionRed); + return(AI_ACTION_LEAVE_WATER_GAS); + } + } + + + //////////////////////////////////////////////////////////////////////////// + // REGULAR CIVILIANS COWER / RUN AWAY + //////////////////////////////////////////////////////////////////////////// + if (!(pSoldier->ubBodyType == COW || pSoldier->ubBodyType == CRIPPLECIV || pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Civilian decisions]"), gLogDecideActionRed); + if (FindAIUsableObjClass(pSoldier, IC_WEAPON) == NO_SLOT) + { + // cower in fear!! + if (pSoldier->flags.uiStatusFlags & SOLDIER_COWERING) + { + if (gfTurnBasedAI || gTacticalStatus.fEnemyInSector) // battle! + { + // in battle! + if (pSoldier->aiData.bLastAction == AI_ACTION_COWER) + { + // do nothing + DebugAI(AI_MSG_INFO, pSoldier, String("Already cowering, do nothing"), gLogDecideActionRed); + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); + } + else + { + // set up next action to run away + pSoldier->aiData.usNextActionData = FindSpotMaxDistFromOpponents(pSoldier); + if (!TileIsOutOfBounds(pSoldier->aiData.usNextActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop cowering. Prepare for running away"), gLogDecideActionRed); + pSoldier->aiData.bNextAction = AI_ACTION_RUN_AWAY; + pSoldier->aiData.usActionData = ANIM_STAND; + return(AI_ACTION_STOP_COWERING); + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Do nothing"), gLogDecideActionRed); + return(AI_ACTION_NONE); + } + } + } + else + { + if (pSoldier->aiData.bNewSituation == NOT_NEW_SITUATION) + { + // stop cowering, not in battle, timer expired + // we have to turn off whatever is necessary to stop status red... + pSoldier->aiData.bAlertStatus = STATUS_GREEN; + return(AI_ACTION_STOP_COWERING); + } + else + { + return(AI_ACTION_NONE); + } + } + } + else + { + if (gfTurnBasedAI || gTacticalStatus.fEnemyInSector) + { + // battle - cower!!! + DebugAI(AI_MSG_INFO, pSoldier, String("Start cowering"), gLogDecideActionRed); + pSoldier->aiData.usActionData = ANIM_CROUCH; + return(AI_ACTION_COWER); + } + else // not in battle, cower for a certain length of time + { + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = (UINT16)REALTIME_CIV_AI_DELAY; + pSoldier->aiData.usActionData = ANIM_CROUCH; + return(AI_ACTION_COWER); + } + } + } + } + + + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: calculate morale"); + pSoldier->aiData.bAIMorale = CalcMorale(pSoldier); + + // if a guy is feeling REALLY discouraged, he may continue to run like hell + if ((pSoldier->aiData.bAIMorale == MORALE_HOPELESS) && ubCanMove) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Low morale, attempting to run away]"), gLogDecideActionRed); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: run away"); + //////////////////////////////////////////////////////////////////////// + // RUN AWAY TO SPOT FARTHEST FROM KNOWN THREATS (ONLY IF MORALE HOPELESS) + //////////////////////////////////////////////////////////////////////// + + // look for best place to RUN AWAY to (farthest from the closest threat) + pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Running away to grid %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + return(AI_ACTION_RUN_AWAY); + } + } + + + // civilians are only interested in running away + INT32 sOpponentGridNo; + INT8 bOpponentLevel; + INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel); + DebugAI(AI_MSG_INFO, pSoldier, String("sClosestOpponent %d", sClosestOpponent), gLogDecideActionRed); + + if ((pSoldier->aiData.bUnderFire || (!TileIsOutOfBounds(sClosestOpponent) && PythSpacesAway(pSoldier->sGridNo, sClosestOpponent) < TACTICAL_RANGE / 2))) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[civilians run away]"), gLogDecideActionRed); + + // look for best place to RUN AWAY to (farthest from the closest threat) + pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); + DebugAI(AI_MSG_INFO, pSoldier, String("found run away spot %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Running away!"), gLogDecideActionRed); + return(AI_ACTION_RUN_AWAY); + } + else if (!SkipCoverCheck && gfTurnBasedAI) // only do in turnbased + { + DebugAI(AI_MSG_INFO, pSoldier, String("Can't run away, try to take cover"), gLogDecideActionRed); + // try to take cover + pSoldier->aiData.bAIMorale = MORALE_WORRIED; + INT32 iDummy; + pSoldier->aiData.usActionData = FindBestNearbyCover(pSoldier, MORALE_WORRIED, &iDummy); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Take cover"), gLogDecideActionRed); + return(AI_ACTION_TAKE_COVER); + } + } + } + + + + /* JULY 29, 1996 - Decided that this was a bad idea, after watching a civilian + start a random patrol while 2 steps away from a hidden armed opponent...*/ + + //////////////////////////////////////////////////////////////////////////// + // SWITCH TO GREEN: soldier does ordinary regular patrol, seeks friends + //////////////////////////////////////////////////////////////////////////// + BOOLEAN fClimb; + // if not in combat or under fire, and we COULD have moved, just chose not to + if ((pSoldier->aiData.bAlertStatus != STATUS_BLACK) && !pSoldier->aiData.bUnderFire && ubCanMove && + (!gfTurnBasedAI || pSoldier->bActionPoints >= pSoldier->bInitialActionPoints) && + (TileIsOutOfBounds(ClosestReachableDisturbance(pSoldier, &fClimb)))) + { + // addition: if soldier is bleeding then reduce bleeding and do nothing + if (pSoldier->bBleeding > MIN_BLEEDING_THRESHOLD) + { + // reduce bleeding by 1 point per AP (in RT, APs will get recalculated so it's okay) + pSoldier->bBleeding = __max(0, pSoldier->bBleeding - (pSoldier->bActionPoints / 2)); + return(AI_ACTION_NONE); // will end-turn/wait depending on whether we're in TB or realtime + } + // Skip RED until new situation/next turn, 30% extra chance to do GREEN actions + pSoldier->aiData.bBypassToGreen = 30; + return(DecideActionGreenCivilian(pSoldier)); + } + + + //////////////////////////////////////////////////////////////////////////// + // DO NOTHING: Not enough points left to move, so save them for next turn + //////////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_INFO, pSoldier, String("Do nothing"), gLogDecideActionRed); + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); +} + +INT8 DecideActionBlackCivilian(SOLDIERTYPE* pSoldier) +{ + DebugAI(AI_MSG_START, pSoldier, String("[Black Civilian]")); + LogDecideInfo(pSoldier); + + INT32 sOpponentGridNo; + INT8 bOpponentLevel; + INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel); + DebugAI(AI_MSG_INFO, pSoldier, String("sClosestOpponent %d", sClosestOpponent)); + + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // sevenfm: initialize data + pSoldier->bWeaponMode = WM_NORMAL; + + + // if we have absolutely no action points, we can't do a thing under BLACK! + if (pSoldier->bActionPoints <= 0) + { + pSoldier->aiData.usActionData = NOWHERE; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_NONE); + } + + // can this guy move to any of the neighbouring squares ? (sets TRUE/FALSE) + UINT8 ubCanMove = (pSoldier->bActionPoints >= MinPtsToMove(pSoldier)); + if (pSoldier->flags.uiStatusFlags & (SOLDIER_DRIVER | SOLDIER_PASSENGER)) + { + ubCanMove = 0; + } + + + INT8 bInWater, bInDeepWater, bInGas; + // determine if we happen to be in water (in which case we're in BIG trouble!) + bInWater = Water(pSoldier->sGridNo, pSoldier->pathing.bLevel); + bInDeepWater = WaterTooDeepForAttacks(pSoldier->sGridNo, pSoldier->pathing.bLevel); + + // calculate our morale + pSoldier->aiData.bAIMorale = CalcMorale(pSoldier); + + //////////////////////////////////////////////////////////////////////////// + // WHEN LEFT IN GAS, WEAR GAS MASK IF AVAILABLE AND NOT WORN + //////////////////////////////////////////////////////////////////////////// + bInGas = DecideActionWearGasmask(pSoldier); + + + //////////////////////////////////////////////////////////////////////////// + // STUCK IN WATER OR GAS, NO COVER, GO TO NEAREST SPOT OF UNGASSED LAND + //////////////////////////////////////////////////////////////////////////// + auto decision = DecideActionStuckInWaterOrGas(pSoldier, ubCanMove, bInWater, bInDeepWater, bInGas); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + + + //////////////////////////////////////////////////////////////////////////// + // CIVILIANS CANNOT ATTACK ANYTHING + //////////////////////////////////////////////////////////////////////////// + if (pSoldier->ubBodyType != COW && pSoldier->ubBodyType != CRIPPLECIV && !(pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE)) + { + // cower in fear!! + if (pSoldier->flags.uiStatusFlags & SOLDIER_COWERING) + { + if (pSoldier->aiData.bLastAction == AI_ACTION_COWER) + { + // do nothing + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); + } + else + { + // set up next action to run away + pSoldier->aiData.usNextActionData = FindSpotMaxDistFromOpponents(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usNextActionData)) + { + pSoldier->aiData.bNextAction = AI_ACTION_RUN_AWAY; + pSoldier->aiData.usActionData = ANIM_STAND; + return(AI_ACTION_STOP_COWERING); + } + else + { + return(AI_ACTION_NONE); + } + } + } + else + { + // cower!!! + pSoldier->aiData.usActionData = ANIM_CROUCH; + return(AI_ACTION_COWER); + } + } + + + //////////////////////////////////////////////////////////////////////////// + // DO NOTHING: Not enough points left to move, so save them for next turn + //////////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Nothing to do]")); + // by default, if everything else fails, just stand in place and wait + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); +} + + +//----------------------------------------------------------------- +// Robot AI decision routines +//----------------------------------------------------------------- +INT8 DecideActionGreenRobot(SOLDIERTYPE* pSoldier) +{ + INT32 iChance, iSneaky = 10; + DebugAI(AI_MSG_START, pSoldier, String("[Green Robot]")); + LogDecideInfo(pSoldier); + + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // sevenfm: initialize data + pSoldier->bWeaponMode = WM_NORMAL; + + gubNPCPathCount = 0; + + + INT8 bInWater = Water(pSoldier->sGridNo, pSoldier->pathing.bLevel); + INT8 bInDeepWater = DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel); + INT8 bInGas = InGasOrSmoke(pSoldier, pSoldier->sGridNo); + + //ddd{ + if (!(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT) && gGameExternalOptions.bNewTacticalAIBehavior && pSoldier->bTeam == ENEMY_TEAM) + { + if (!(gTacticalStatus.uiFlags & TURNBASED) && (gTacticalStatus.uiFlags & INCOMBAT)) + { + INT32 cnt; + ROTTING_CORPSE* pCorpse; + + for (cnt = 0; cnt < giNumRottingCorpse; ++cnt) + { + pCorpse = &(gRottingCorpse[cnt]); + + if (pCorpse->fActivated && pCorpse->def.ubAIWarningValue > 0) + { + if (PythSpacesAway(pSoldier->sGridNo, pCorpse->def.sGridNo) <= 5)//add check(comparison) of sight range variable (smaxvid ?) + { + //check if the corpse is in the enemny/militia field of view? + //CHRISL: Shouldn't we be using the corpse's bLevel? Otherwise a soldier inside a building can see a corpse on the roof of that building + //if ( SoldierTo3DLocationLineOfSightTest( pSoldier, pCorpse->def.sGridNo, pSoldier->pathing.bLevel, 3, TRUE, CALC_FROM_WANTED_DIR ) ) + if (SoldierTo3DLocationLineOfSightTest(pSoldier, pCorpse->def.sGridNo, pCorpse->def.bLevel, 3, TRUE, CALC_FROM_WANTED_DIR)) + { + ScreenMsg(MSG_FONT_YELLOW, MSG_INTERFACE, New113Message[MSG113_ENEMY_FOUND_DEAD_BODY]); + //pCorpse->def.ubAIWarningValue=0; + gRottingCorpse[cnt].def.ubAIWarningValue = 0; + return(AI_ACTION_RED_ALERT); + } + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // IF YOU SEE CAPTURED FRIENDS, FREE THEM! + //////////////////////////////////////////////////////////////////////////// + + // Flugente: if we see one of our buddies in handcuffs, its a clear sign of enemy activity! + if (gGameExternalOptions.fAllowPrisonerSystem && pSoldier->bTeam == ENEMY_TEAM && !gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition) + { + SoldierID ubPerson = GetClosestFlaggedSoldierID(pSoldier, 20, ENEMY_TEAM, SOLDIER_POW, TRUE); + + if (ubPerson != NOBODY) + { + // raise alarm! + return(AI_ACTION_RED_ALERT); + } + } + + // are we a bodyguard? + if (pSoldier->usSoldierFlagMask & SOLDIER_BODYGUARD) + { + // is VIP still alive? + SoldierID ubPerson = GetClosestFlaggedSoldierID(pSoldier, 100, pSoldier->bTeam, SOLDIER_VIP, FALSE); + + if (ubPerson != NOBODY) + { + // we want to stay close to him, but still be able to function properly... stay withing a 7-tile radius + if (SpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) > 7) + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + } + //ddd} + + //////////////////////////////////////////////////////////////////////////// + // POINT PATROL: move towards next point unless getting a bit winded + //////////////////////////////////////////////////////////////////////////// + + // this takes priority over water/gas checks, so that point patrol WILL work + // from island to island, and through gas covered areas, too + if ((pSoldier->aiData.bOrders == POINTPATROL) && (pSoldier->bBreath >= 75)) + { + if (PointPatrolAI(pSoldier)) + { + if (!gfTurnBasedAI) + { + // wait after this... + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = RealtimeDelay(pSoldier); + } + return(AI_ACTION_POINT_PATROL); + } + else + { + // Reset path count to avoid dedlok + gubNPCPathCount = 0; + } + } + + if ((pSoldier->aiData.bOrders == RNDPTPATROL) && (pSoldier->bBreath >= 75)) + { + if (RandomPointPatrolAI(pSoldier)) + { + if (!gfTurnBasedAI) + { + // wait after this... + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = RealtimeDelay(pSoldier); + } + return(AI_ACTION_POINT_PATROL); + } + else + { + // Reset path count to avoid dedlok + gubNPCPathCount = 0; + } + + } + + //////////////////////////////////////////////////////////////////////////// + // WHEN LEFT IN WATER OR GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND + //////////////////////////////////////////////////////////////////////////// + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: get out of water and gas")); + + if (bInDeepWater || bInGas || FindBombNearby(pSoldier, pSoldier->sGridNo, BOMB_DETECTION_RANGE) || RedSmokeDanger(pSoldier->sGridNo, pSoldier->pathing.bLevel)) + { + pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - SEEKING NEAREST UNGASSED LAND at grid %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + return(AI_ACTION_LEAVE_WATER_GAS); + } + } + + + + //////////////////////////////////////////////////////////////////////////// + // RANDOM PATROL: determine % chance to start a new patrol route + //////////////////////////////////////////////////////////////////////////// + if (!gubNPCPathCount) // try to limit pathing in Green AI + { + + iChance = 25 + pSoldier->aiData.bBypassToGreen; + + // set base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += -20; break; + case ONGUARD: iChance += -15; break; + case ONCALL: break; + case CLOSEPATROL: iChance += +15; break; + case RNDPTPATROL: + case POINTPATROL: iChance = 0; break; + case FARPATROL: iChance += +25; break; + case SEEKENEMY: iChance += -10; break; + case SNIPER: iChance += -10; break; + } + + // modify chance of patrol (and whether it's a sneaky one) by attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += -10; break; + case BRAVESOLO: iChance += 5; break; + case BRAVEAID: break; + case CUNNINGSOLO: iChance += 5; iSneaky += 10; break; + case CUNNINGAID: iSneaky += 5; break; + case AGGRESSIVE: iChance += 10; iSneaky += -5; break; + case ATTACKSLAYONLY: iChance += 10; iSneaky += -5; break; + } + + // reduce chance for any injury, less likely to wander around when hurt + iChance -= (pSoldier->stats.bLifeMax - pSoldier->stats.bLife); + + // reduce chance if breath is down, less likely to wander around when tired + iChance -= (100 - pSoldier->bBreath); + + + // if we're in water with land miles (> 25 tiles) away, + // OR if we roll under the chance calculated + if (bInWater || ((INT16)PreRandom(100) < iChance)) + { + pSoldier->aiData.usActionData = RandDestWithinRange(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, pSoldier->aiData.usActionData, AI_ACTION_RANDOM_PATROL); + } + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + if (!gfTurnBasedAI) + { + // wait after this... + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = RealtimeDelay(pSoldier); + } + return(AI_ACTION_RANDOM_PATROL); + } + } + } + + if (!gubNPCPathCount) // try to limit pathing in Green AI + { + //////////////////////////////////////////////////////////////////////////// + // SEEK FRIEND: determine %chance for man to pay a friendly visit + //////////////////////////////////////////////////////////////////////////// + + iChance = 25 + pSoldier->aiData.bBypassToGreen; + + // set base chance and maximum seeking distance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += -20; break; + case ONGUARD: iChance += -15; break; + case ONCALL: break; + case CLOSEPATROL: iChance += +10; break; + case RNDPTPATROL: + case POINTPATROL: iChance = -10; break; + case FARPATROL: iChance += +20; break; + case SEEKENEMY: iChance += -10; break; + case SNIPER: iChance += -10; break; + } + + // modify for attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: break; + case BRAVESOLO: iChance /= 2; break; // loners + case BRAVEAID: iChance += 10; break; // friendly + case CUNNINGSOLO: iChance /= 2; break; // loners + case CUNNINGAID: iChance += 10; break; // friendly + case AGGRESSIVE: break; + case ATTACKSLAYONLY: break; + } + + // reduce chance for any injury, less likely to wander around when hurt + iChance -= (pSoldier->stats.bLifeMax - pSoldier->stats.bLife); + + // reduce chance if breath is down + iChance -= (100 - pSoldier->bBreath); // very likely to wait when exhausted + + + if ((INT16)PreRandom(100) < iChance) + { + if (RandomFriendWithin(pSoldier)) + { + if (pSoldier->aiData.usActionData == GoAsFarAsPossibleTowards(pSoldier, pSoldier->aiData.usActionData, AI_ACTION_SEEK_FRIEND)) + { + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + + + + //////////////////////////////////////////////////////////////////////////// + // LOOK AROUND: determine %chance for man to turn in place + //////////////////////////////////////////////////////////////////////////// + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Soldier deciding to turn")); + if (!gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) + { + // avoid 2 consecutive random turns in a row + if (pSoldier->aiData.bLastAction != AI_ACTION_CHANGE_FACING) + { + iChance = 25 + pSoldier->aiData.bBypassToGreen; + + // set base chance according to orders + if (pSoldier->aiData.bOrders == STATIONARY || pSoldier->aiData.bOrders == SNIPER) + iChance += 25; + + if (pSoldier->aiData.bOrders == ONGUARD) + iChance += 20; + + if (pSoldier->aiData.bAttitude == DEFENSIVE) + iChance += 25; + + if (pSoldier->aiData.bOrders == SNIPER && pSoldier->pathing.bLevel == 1) + iChance += 35; + + if (WeaponReady(pSoldier)) // SANDRO - if readied weapon, make him more likely to turn around + iChance += 30; + + if ((INT16)PreRandom(100) < iChance) + { + // roll random directions (stored in actionData) until different from current + do + { + // if man has a LEGAL dominant facing, and isn't facing it, he will turn + // back towards that facing 50% of the time here (normally just enemies) + if ((pSoldier->aiData.bDominantDir >= 0) && (pSoldier->aiData.bDominantDir <= 8) && + (pSoldier->ubDirection != pSoldier->aiData.bDominantDir) && PreRandom(2) && pSoldier->aiData.bOrders != SNIPER) + { + pSoldier->aiData.usActionData = pSoldier->aiData.bDominantDir; + } + else + { + INT32 iNoiseValue; + BOOLEAN fClimb; + BOOLEAN fReachable; + INT32 sNoiseGridNo = MostImportantNoiseHeard(pSoldier, &iNoiseValue, &fClimb, &fReachable); + UINT8 ubNoiseDir; + + if (TileIsOutOfBounds(sNoiseGridNo) || + (ubNoiseDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sNoiseGridNo)) == pSoldier->ubDirection) + + { + pSoldier->aiData.usActionData = PreRandom(8); + } + else + { + pSoldier->aiData.usActionData = ubNoiseDir; + } + } + } while (pSoldier->aiData.usActionData == pSoldier->ubDirection); + + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Trying to turn - checking stance validity, sniper = %d", pSoldier->sniper)); + if (pSoldier->InternalIsValidStance((INT8)pSoldier->aiData.usActionData, gAnimControl[pSoldier->usAnimState].ubEndHeight)) + { + + if (!gfTurnBasedAI) + { + // wait after this... + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = RealtimeDelay(pSoldier); + } + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Soldier is turning")); + return(AI_ACTION_CHANGE_FACING); + } + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // NONE: + //////////////////////////////////////////////////////////////////////////// + + // by default, if everything else fails, just stands in place without turning + // for realtime, regular AI guys will use a standard wait set outside of here + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); +} + +INT8 DecideActionYellowRobot(SOLDIERTYPE* pSoldier) +{ + INT32 iDummy; + UINT8 ubNoiseDir; + INT32 sNoiseGridNo; + INT32 iNoiseValue; + INT32 iChance, iSneaky; + INT32 sClosestFriend; + BOOLEAN fClimb; + BOOLEAN fReachable; +#ifdef DEBUGDECISIONS + STR16 tempstr; +#endif + + DebugAI(AI_MSG_START, pSoldier, String("[Yellow Robot]")); + LogDecideInfo(pSoldier); + + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // sevenfm: initialize data + pSoldier->bWeaponMode = WM_NORMAL; + + + //////////////////////////////////////////////////////////////////////////// + // WHEN IN GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND + //////////////////////////////////////////////////////////////////////////// + // Except robots do not care about gas + if (InGas(pSoldier, pSoldier->sGridNo) || DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel) || FindBombNearby(pSoldier, pSoldier->sGridNo, BOMB_DETECTION_RANGE)) + { + pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_LEAVE_WATER_GAS); + } + } + + // determine the most important noise heard, and its relative value + sNoiseGridNo = MostImportantNoiseHeard(pSoldier, &iNoiseValue, &fClimb, &fReachable); + //NumMessage("iNoiseValue = ",iNoiseValue); + + if (TileIsOutOfBounds(sNoiseGridNo)) + { + // then we have no business being under YELLOW status any more! + return(AI_ACTION_NONE); + } + + if (gGameExternalOptions.bNewTacticalAIBehavior) + { + //////////////////////////////////////////////////////////////////////////// + // IF YOU SEE CAPTURED FRIENDS, FREE THEM! + //////////////////////////////////////////////////////////////////////////// + // Flugente: if we see one of our buddies captured, it is a clear sign of enemy activity! + if (gGameExternalOptions.fAllowPrisonerSystem && pSoldier->bTeam == ENEMY_TEAM) + { + SoldierID ubPerson = GetClosestFlaggedSoldierID(pSoldier, 20, ENEMY_TEAM, SOLDIER_POW, TRUE); + + if (ubPerson != NOBODY) + { + // if we are close, we can release this guy + // possible only if not handcuffed (binders can be opened, handcuffs not) + if (!HasItemFlag((&(ubPerson->inv[HANDPOS]))->usItem, HANDCUFFS)) + { + if (PythSpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) < 2) + { + // see if we are facing this person + UINT8 ubDesiredMercDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, ubPerson->sGridNo); + + // if not already facing in that direction, + if (pSoldier->ubDirection != ubDesiredMercDir) + { + pSoldier->aiData.usActionData = ubDesiredMercDir; + + return(AI_ACTION_CHANGE_FACING); + } + + return(AI_ACTION_FREE_PRISONER); + } + else + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_SEEK_FRIEND); + } + } + } + else if (!(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT) && !gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition) + { + // raise alarm! + return(AI_ACTION_RED_ALERT); + } + } + } + + // are we a bodyguard? + if (pSoldier->usSoldierFlagMask & SOLDIER_BODYGUARD) + { + // is VIP still alive? + SoldierID ubPerson = GetClosestFlaggedSoldierID(pSoldier, 100, pSoldier->bTeam, SOLDIER_VIP, FALSE); + + if (ubPerson != NOBODY) + { + // we want to stay close to him, but still be able to function properly... stay withing a 7-tile radius + if (SpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) > 7) + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // LOOK AROUND TOWARD NOISE: determine %chance for man to turn towards noise + //////////////////////////////////////////////////////////////////////////// + + // determine direction from this soldier in which the noise lies + ubNoiseDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sNoiseGridNo); + + // if soldier is not already facing in that direction, + // and the noise source is close enough that it could possibly be seen + if (!gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) + { + if ((pSoldier->ubDirection != ubNoiseDir) && PythSpacesAway(pSoldier->sGridNo, sNoiseGridNo) <= pSoldier->GetMaxDistanceVisible(sNoiseGridNo)) + { + // set base chance according to orders + if ((pSoldier->aiData.bOrders == STATIONARY) || (pSoldier->aiData.bOrders == ONGUARD)) + iChance = 50; + else // all other orders + iChance = 25; + + if (pSoldier->aiData.bAttitude == DEFENSIVE) + iChance += 15; + + + if ((INT16)PreRandom(100) < iChance && pSoldier->InternalIsValidStance(ubNoiseDir, gAnimControl[pSoldier->usAnimState].ubEndHeight)) + { + pSoldier->aiData.usActionData = ubNoiseDir; + if (pSoldier->aiData.bOrders == SNIPER && + (pSoldier->bBreath > 25 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 30) && + !WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) + { + if (!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, READY_RIFLE_CROUCH) <= pSoldier->bActionPoints) + { + pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; + } + } + //////////////////////////////////////////////////////////////////////////// + // SANDRO - allow regular soldiers to raise scoped weapons to see farther away too + if (IsScoped(&pSoldier->inv[HANDPOS])) + { + if (!WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION && + (pSoldier->bBreath > 25 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 30)) + { + if (!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, PickSoldierReadyAnimation(pSoldier, FALSE, FALSE)) <= pSoldier->bActionPoints) + { + if (Random(100) < 35) + { + pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; + } + } + } + } + //////////////////////////////////////////////////////////////////////////// + + return(AI_ACTION_CHANGE_FACING); + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // RADIO YELLOW ALERT: determine %chance to call others and report noise + //////////////////////////////////////////////////////////////////////////// + + // if we have the action points remaining to RADIO + // (we never want NPCs to choose to radio if they would have to wait a turn) + if ((pSoldier->bActionPoints >= APBPConstants[AP_RADIO]) && + (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1)) + { + // base chance depends on how much new info we have to radio to the others + iChance = 5 * WhatIKnowThatPublicDont(pSoldier, FALSE); // use 5 * for YELLOW alert + + // if I actually know something they don't and I ain't swimming (deep water) + if (iChance && !DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel)) + { + + // CJC: this addition allows for varying difficulty levels for soldier types + iChance += gbDiff[DIFF_RADIO_RED_ALERT][SoldierDifficultyLevel(pSoldier)] / 2; + + // Alex: this addition replaces the sectorValue/2 in original JA + //iChance += gsDiff[DIFF_RADIO_RED_ALERT][GameOption[ENEMYDIFFICULTY]] / 2; + + // modify base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += 20; break; + case ONGUARD: iChance += 15; break; + case ONCALL: iChance += 10; break; + case CLOSEPATROL: break; + case RNDPTPATROL: + case POINTPATROL: break; + case FARPATROL: iChance += -10; break; + case SEEKENEMY: iChance += -20; break; + case SNIPER: iChance += -10; break; //Madd: sniper contacts are supposed to be automatically reported + } + + // modify base chance according to attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += 20; break; + case BRAVESOLO: iChance += -10; break; + case BRAVEAID: break; + case CUNNINGSOLO:iChance += -5; break; + case CUNNINGAID: break; + case AGGRESSIVE: iChance += -20; break; + case ATTACKSLAYONLY: iChance = 0; break; + } + +#ifdef DEBUGDECISIONS + AINumMessage("Chance to radio yellow alert = ", iChance); +#endif + + if ((INT16)PreRandom(100) < iChance) + { +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "decides to radio a YELLOW alert!", 1000); +#endif + + return(AI_ACTION_YELLOW_ALERT); + } + } + } + + + //continue flanking + INT32 sFlankGridNo; + + if (TileIsOutOfBounds(sNoiseGridNo)) + sFlankGridNo = pSoldier->lastFlankSpot; + else + sFlankGridNo = sNoiseGridNo; + + if (pSoldier->numFlanks > 0 && pSoldier->numFlanks < MAX_FLANKS_YELLOW) + { + INT16 currDir = GetDirectionFromGridNo(sFlankGridNo, pSoldier); + INT16 origDir = pSoldier->origDir; + pSoldier->numFlanks += 1; + if (pSoldier->flags.lastFlankLeft) + { + if (origDir > currDir) + origDir -= NUM_WORLD_DIRECTIONS; + + // stop flanking if reached desired direction + if ((currDir - origDir) >= MinFlankDirections(pSoldier)) + { + pSoldier->numFlanks = MAX_FLANKS_YELLOW; + } + else + { + pSoldier->aiData.usActionData = FindFlankingSpot(pSoldier, sFlankGridNo, AI_ACTION_FLANK_LEFT); + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) //&& (currDir - origDir) < 2 ) + return AI_ACTION_FLANK_LEFT; + else + pSoldier->numFlanks = MAX_FLANKS_YELLOW; + } + } + else + { + if (origDir < currDir) + origDir += NUM_WORLD_DIRECTIONS; + + // stop flanking if reached desired direction + if ((origDir - currDir) >= MinFlankDirections(pSoldier)) + { + pSoldier->numFlanks = MAX_FLANKS_YELLOW; + } + else + { + pSoldier->aiData.usActionData = FindFlankingSpot(pSoldier, sFlankGridNo, AI_ACTION_FLANK_RIGHT); + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))//&& (origDir - currDir) < 2 ) + return AI_ACTION_FLANK_RIGHT; + else + pSoldier->numFlanks = MAX_FLANKS_YELLOW; + } + } + } + + if (pSoldier->numFlanks == MAX_FLANKS_YELLOW) + { + pSoldier->numFlanks += 1; + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sFlankGridNo, AI_ACTION_SEEK_NOISE); + return AI_ACTION_SEEK_NOISE; + } + + // Hmmm, I don't think this check is doing what is intended. But then I see no comment about what is intended. + // However, civilians with no profile (and likely no weapons) do not need to be seeking out noises. Most don't + // even have the body type for it (can't climb or jump). + //if ( !( pSoldier->bTeam == CIV_TEAM && pSoldier->ubProfile != NO_PROFILE && pSoldier->ubProfile != ELDIN ) ) + //if ( pSoldier->bTeam != CIV_TEAM || ( !pSoldier->aiData.bNeutral && pSoldier->ubProfile != ELDIN ) ) + // ADB: Eldin is the only neutral civilian who should be seeking out noises. As the museum curator, he can be + // available to talk to. As the night watchman, he needs to look for thieves. + bool onCivTeam = (pSoldier->bTeam == CIV_TEAM); + bool isNamedCiv = (pSoldier->ubProfile != NO_PROFILE); + bool isEldin = (pSoldier->ubProfile == ELDIN);//logically flipped from the original, isNotEldin == false is confusing + // For purpose of seeking noise, cowardly civs are neutral, even if attacked by your thugs + bool isNeutral = pSoldier->aiData.bNeutral || pSoldier->flags.uiStatusFlags & SOLDIER_COWERING; + if ( + (onCivTeam == false) || //true #1 + (onCivTeam == true && isNamedCiv == true && isNeutral == false) || //true #2 + (onCivTeam == true && isEldin == true)//true #3 + ) + { + // IF WE ARE MILITIA/CIV IN REALTIME, CLOSE TO NOISE, AND CAN SEE THE SPOT WHERE THE NOISE CAME FROM, FORGET IT + if (fReachable && !fClimb && !gfTurnBasedAI && (pSoldier->bTeam == MILITIA_TEAM || pSoldier->bTeam == CIV_TEAM) && PythSpacesAway(pSoldier->sGridNo, sNoiseGridNo) < 5) + { + if (SoldierTo3DLocationLineOfSightTest(pSoldier, sNoiseGridNo, pSoldier->pathing.bLevel, 0, TRUE, 6)) + { + // set reachable to false so we don't investigate + fReachable = FALSE; + // forget about noise + pSoldier->aiData.sNoiseGridno = NOWHERE; + pSoldier->aiData.ubNoiseVolume = 0; + } + } + + //////////////////////////////////////////////////////////////////////////// + // SEEK NOISE + //////////////////////////////////////////////////////////////////////////// + + if (fReachable) + { + // remember that noise value is negative, and closer to 0 => more important! + iChance = 95 + (iNoiseValue / 3); + iSneaky = 30; + + // increase + + // set base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += -20; break; + case ONGUARD: iChance += -15; break; + case ONCALL: break; + case CLOSEPATROL: iChance += -10; break; + case RNDPTPATROL: + case POINTPATROL: break; + case FARPATROL: iChance += 10; break; + case SEEKENEMY: iChance += 25; break; + case SNIPER: iChance += -10; break; + } + + // modify chance of patrol (and whether it's a sneaky one) by attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += -10; iSneaky += 15; break; + case BRAVESOLO: iChance += 10; break; + case BRAVEAID: iChance += 5; break; + case CUNNINGSOLO: iChance += 5; iSneaky += 30; break; + case CUNNINGAID: iSneaky += 30; break; + case AGGRESSIVE: iChance += 20; iSneaky += -10; break; + case ATTACKSLAYONLY: iChance += 20; iSneaky += -10; break; + } + + + // reduce chance if breath is down, less likely to wander around when tired + iChance -= (100 - pSoldier->bBreath); + + //Madd: make militia less likely to go running headlong into trouble + if (pSoldier->bTeam == MILITIA_TEAM) + iChance -= 30; + + if ((INT16)PreRandom(100) < iChance) + { + + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sNoiseGridNo, AI_ACTION_SEEK_NOISE); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - INVESTIGATING NOISE at grid %d, moving to %d", + pSoldier->name, sNoiseGridNo, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + if (!ENEMYROBOT(pSoldier) && fClimb)//&& pSoldier->aiData.usActionData == sNoiseGridNo) + { + // need to climb AND have enough APs to get there this turn + BOOLEAN fUp = TRUE; + if (pSoldier->pathing.bLevel > 0) + fUp = FALSE; + + if (!fUp) + DebugMsg(TOPIC_JA2AI, DBG_LEVEL_3, String("Soldier %d, is climbing down", pSoldier->ubID)); + + // 0verhaul: the Closest Noise call returns the location of a climb. So 1) it's not necessary to + // ask if we can climb from here. And 2) It's not necessary to look for the climb point. We already + // have it. +// if ( CanClimbFromHere ( pSoldier, fUp ) ) + if (pSoldier->sGridNo == sNoiseGridNo) + { + if (IsActionAffordable(pSoldier) && pSoldier->bActionPoints >= (APBPConstants[AP_CLIMBROOF] + MinAPsToAttack(pSoldier, sNoiseGridNo, ADDTURNCOST, 0))) + { + return(AI_ACTION_CLIMB_ROOF); + } + } + else + { + // pSoldier->aiData.usActionData = FindClosestClimbPoint(pSoldier, pSoldier->sGridNo , sNoiseGridNo , fUp ); + pSoldier->aiData.usActionData = sNoiseGridNo; + //if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_MOVE_TO_CLIMB); + } + } + } + + // possibly start YELLOW flanking + if (gGameExternalOptions.fAIYellowFlanking && + (pSoldier->aiData.bAttitude == CUNNINGAID || pSoldier->aiData.bAttitude == CUNNINGSOLO) && + pSoldier->bTeam == ENEMY_TEAM && + (CountFriendsInDirection(pSoldier, sNoiseGridNo) > 0 || NightTime()) && + (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); + if (dist > MIN_FLANK_DIST_YELLOW && dist < MAX_FLANK_DIST_YELLOW) + { + INT16 rdm = Random(6); + + switch (rdm) + { + case 1: + case 2: + case 3: + if (pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT) + action = AI_ACTION_FLANK_LEFT; + break; + default: + if (pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT) + action = AI_ACTION_FLANK_RIGHT; + break; + } + } + else + return AI_ACTION_SEEK_NOISE; + + pSoldier->aiData.usActionData = FindFlankingSpot(pSoldier, sNoiseGridNo, action); + + if (TileIsOutOfBounds(pSoldier->aiData.usActionData) || pSoldier->numFlanks >= MAX_FLANKS_YELLOW) + { + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sNoiseGridNo, AI_ACTION_SEEK_NOISE); + //pSoldier->numFlanks = 0; + return(AI_ACTION_SEEK_NOISE); + } + else + { + if (action == AI_ACTION_FLANK_LEFT) + pSoldier->flags.lastFlankLeft = TRUE; + else + pSoldier->flags.lastFlankLeft = FALSE; + + if (pSoldier->lastFlankSpot != sNoiseGridNo) + pSoldier->numFlanks = 0; + + pSoldier->origDir = GetDirectionFromGridNo(sNoiseGridNo, pSoldier); + pSoldier->lastFlankSpot = sNoiseGridNo; + pSoldier->numFlanks++; + + // sevenfm: change orders CLOSEPATROL -> FARPATROL + if (pSoldier->aiData.bOrders == CLOSEPATROL) + { + pSoldier->aiData.bOrders = FARPATROL; + } + + return(action); + } + } + else + { + return(AI_ACTION_SEEK_NOISE); + } + + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // SEEK FRIEND WHO LAST RADIOED IN TO REPORT NOISE + //////////////////////////////////////////////////////////////////////////// + + sClosestFriend = ClosestReachableFriendInTrouble(pSoldier, &fClimb); + + // if there is a friend alive & reachable who last radioed in + if (!TileIsOutOfBounds(sClosestFriend)) + { + // there a chance enemy soldier choose to go "help" his friend + iChance = 50 - SpacesAway(pSoldier->sGridNo, sClosestFriend); + iSneaky = 10; + + // set base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += -20; break; + case ONGUARD: iChance += -15; break; + case ONCALL: iChance += 20; break; + case CLOSEPATROL: iChance += -10; break; + case RNDPTPATROL: + case POINTPATROL: iChance += -10; break; + case FARPATROL: break; + case SEEKENEMY: iChance += 10; break; + case SNIPER: iChance += -10; break; + } + + // modify chance of patrol (and whether it's a sneaky one) by attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += -10; iSneaky += 15; break; + case BRAVESOLO: break; + case BRAVEAID: iChance += 20; iSneaky += -10; break; + case CUNNINGSOLO: iSneaky += 30; break; + case CUNNINGAID: iChance += 20; iSneaky += 20; break; + case AGGRESSIVE: iChance += -20; iSneaky += -20; break; + case ATTACKSLAYONLY: iChance += -20; iSneaky += -20; break; + } + + // reduce chance if breath is down, less likely to wander around when tired + iChance -= (100 - pSoldier->bBreath); + + if ((INT16)PreRandom(100) < iChance) + { + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sClosestFriend, AI_ACTION_SEEK_FRIEND); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - SEEKING FRIEND at %d, MOVING to %d", + pSoldier->name, sClosestFriend, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + if (!ENEMYROBOT(pSoldier) && fClimb)//&& pSoldier->aiData.usActionData == sClosestFriend) + { + // need to climb AND have enough APs to get there this turn + BOOLEAN fUp = TRUE; + if (pSoldier->pathing.bLevel > 0) + fUp = FALSE; + + if (!fUp) + DebugMsg(TOPIC_JA2AI, DBG_LEVEL_3, String("Soldier %d is climbing down", pSoldier->ubID)); + + // 0verhaul: Closest Friend call also returns the climb point if climbing is necessary. So don't + // climb the wrong building and don't search again + //if ( CanClimbFromHere ( pSoldier, fUp ) ) + if (pSoldier->sGridNo == sClosestFriend) + { + if (IsActionAffordable(pSoldier)) + { + return(AI_ACTION_CLIMB_ROOF); + } + } + else + { + //pSoldier->aiData.usActionData = FindClosestClimbPoint(pSoldier, pSoldier->sGridNo , sClosestFriend , fUp ); + pSoldier->aiData.usActionData = sClosestFriend; + //if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_MOVE_TO_CLIMB); + } + } + } + + //if (fClimb && pSoldier->aiData.usActionData == sClosestFriend) + //{ + //// need to climb AND have enough APs to get there this turn + //return( AI_ACTION_CLIMB_ROOF ); + //} + + return(AI_ACTION_SEEK_FRIEND); + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // TAKE BEST NEARBY COVER FROM THE NOISE GENERATING GRIDNO + //////////////////////////////////////////////////////////////////////////// + + if (!SkipCoverCheck) // && gfTurnBasedAI) // only do in turnbased + { + // remember that noise value is negative, and closer to 0 => more important! + iChance = 25; + iSneaky = 30; + + // set base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += 20; break; + case ONGUARD: iChance += 15; break; + case ONCALL: break; + case CLOSEPATROL: iChance += 10; break; + case RNDPTPATROL: + case POINTPATROL: break; + case FARPATROL: iChance += -5; break; + case SEEKENEMY: iChance += -20; break; + case SNIPER: iChance += 20; break; + } + + // modify chance (and whether it's sneaky) by attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += 10; iSneaky += 15; break; + case BRAVESOLO: iChance += -15; iSneaky += -20; break; + case BRAVEAID: iChance += -20; iSneaky += -20; break; + case CUNNINGSOLO: iChance += 20; iSneaky += 30; break; + case CUNNINGAID: iChance += 15; iSneaky += 30; break; + case AGGRESSIVE: iChance += -10; iSneaky += -10; break; + case ATTACKSLAYONLY: iChance += -10; iSneaky += -10; break; + } + + + //Madd: make militia more likely to take cover + if (pSoldier->bTeam == MILITIA_TEAM) + iChance += 20; + + // reduce chance if breath is down, less likely to wander around when tired + iChance -= (100 - pSoldier->bBreath); + + if ((INT16)PreRandom(100) < iChance) + { + pSoldier->aiData.bAIMorale = CalcMorale(pSoldier); + pSoldier->aiData.usActionData = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iDummy); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - TAKING COVER at grid %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + return(AI_ACTION_TAKE_COVER); + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // SWITCH TO GREEN: determine if soldier acts as if nothing at all was wrong + //////////////////////////////////////////////////////////////////////////// + if ((INT16)PreRandom(100) < 50) + { +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "ignores noise completely and BYPASSES to GREEN!", 1000); +#endif + // Skip YELLOW until new situation, 15% extra chance to do GREEN actions + pSoldier->aiData.bBypassToGreen = 15; + return(DecideActionGreenRobot(pSoldier)); + } + + + //////////////////////////////////////////////////////////////////////////// + // DO NOTHING: Not enough points left to move, so save them for next turn + //////////////////////////////////////////////////////////////////////////// + +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "- DOES NOTHING (YELLOW)", 1000); +#endif + + // by default, if everything else fails, just stands in place without turning + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); +} + +INT8 DecideActionRedRobot(SOLDIERTYPE* pSoldier) +{ + DebugAI(AI_MSG_START, pSoldier, String("[Red Robot]"), gLogDecideActionRed); + LogDecideInfo(pSoldier, gLogDecideActionRed); + + ActionType decision = AI_ACTION_INVALID; + + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // sevenfm: initialize data + pSoldier->bWeaponMode = WM_NORMAL; + + // if we have absolutely no action points, we can't do a thing under RED! + if (pSoldier->bActionPoints <= 0) //Action points can be negative + { + pSoldier->aiData.usActionData = NOWHERE; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_NONE); + } + + // sevenfm: find closest opponent + INT32 sOpponentGridNo; + INT8 bOpponentLevel; + INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel); + DebugAI(AI_MSG_INFO, pSoldier, String("sClosestOpponent %d", sClosestOpponent), gLogDecideActionRed); + + BOOLEAN fCanBeSeen = FALSE; + if (!SightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE)) + { + fCanBeSeen = TRUE; + DebugAI(AI_MSG_INFO, pSoldier, String("can be seen"), gLogDecideActionRed); + } + + BOOLEAN fProneSightCover = ProneSightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE); + BOOLEAN fAnyCover = AnyCoverAtSpot(pSoldier, pSoldier->sGridNo); + DebugAI(AI_MSG_INFO, pSoldier, String("prone sight cover %d", fProneSightCover), gLogDecideActionRed); + DebugAI(AI_MSG_INFO, pSoldier, String("any cover %d", fAnyCover), gLogDecideActionRed); + + BOOLEAN fDangerousSpot = FALSE; + if (!fProneSightCover || pSoldier->aiData.bUnderFire) + { + fDangerousSpot = TRUE; + } + + // can this guy move to any of the neighbouring squares ? (sets TRUE/FALSE) + UINT8 ubCanMove = (pSoldier->bActionPoints >= MinPtsToMove(pSoldier)); + + + // determine if we happen to be in water (in which case we're in BIG trouble!) + INT8 bInWater = Water(pSoldier->sGridNo, pSoldier->pathing.bLevel); + INT8 bInDeepWater = DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel); + INT8 bInGas = FALSE; + + //////////////////////////////////////////////////////////////////////////// + // WHEN IN GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND + //////////////////////////////////////////////////////////////////////////// + // when in deep water, move to closest opponent + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide action if stuck in water or gas]"), gLogDecideActionRed); + if (ubCanMove && bInDeepWater && !pSoldier->aiData.bNeutral && pSoldier->aiData.bOrders == SEEKENEMY) + { + // find closest reachable opponent, excluding opponents in deep water + BOOLEAN fClimb; + pSoldier->aiData.usActionData = ClosestReachableDisturbance(pSoldier, &fClimb); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Move out of water towards closest opponent"), gLogDecideActionRed); + return(AI_ACTION_LEAVE_WATER_GAS); + } + } + + if (ubCanMove && (bInGas || bInDeepWater || FindBombNearby(pSoldier, pSoldier->sGridNo, BOMB_DETECTION_RANGE) || RedSmokeDanger(pSoldier->sGridNo, pSoldier->pathing.bLevel))) + { + pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - SEEKING NEAREST UNGASSED LAND at grid %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + DebugAI(AI_MSG_INFO, pSoldier, String("Leave for nearest (ungassed) land"), gLogDecideActionRed); + return(AI_ACTION_LEAVE_WATER_GAS); + } + } + + + + //////////////////////////////////////////////////////////////////////// + // IF POSSIBLE, FIRE LONG RANGE WEAPONS AT TARGETS REPORTED BY RADIO + //////////////////////////////////////////////////////////////////////// + ATTACKTYPE BestThrow, BestShot; + + // can't do this in realtime, because the player could be shooting a gun or whatever at the same time! + if (gfTurnBasedAI && + !bInWater && + !bInGas && + pSoldier->CheckInitialAP() && + !pSoldier->IsFlanking() && + (CanNPCAttack(pSoldier) == TRUE)) + { + BestThrow.ubPossible = FALSE; // by default, assume Throwing isn't possible + DebugAI(AI_MSG_TOPIC, pSoldier, String("[CheckIfTossPossible]"), gLogDecideActionRed); + CheckIfTossPossible(pSoldier, &BestThrow); + + + //////////////////////////////////////////////////////////////////////// + // CHECK IF THROWING A GRENADE OR USING A LAUNCHER/MORTAR AGAINST ENEMY IS POSSIBLE + //////////////////////////////////////////////////////////////////////// + if (BestThrow.ubPossible) + { + DebugAI(AI_MSG_INFO, pSoldier, String("throw possible"), gLogDecideActionRed); + // sevenfm: allow using mortars, grenade launchers, flares and grenades in RED state + if (ItemIsMortar(pSoldier->inv[BestThrow.bWeaponIn].usItem) || + //Item[pSoldier->inv[ BestThrow.bWeaponIn ].usItem].cannon || + ItemIsRocketLauncher(pSoldier->inv[BestThrow.bWeaponIn].usItem) || + ItemIsGrenadeLauncher(pSoldier->inv[BestThrow.bWeaponIn].usItem) || + ItemIsFlare(pSoldier->inv[BestThrow.bWeaponIn].usItem) || + Item[pSoldier->inv[BestThrow.bWeaponIn].usItem].usItemClass & IC_GRENADE) + { + // if firing mortar make sure we have room + if (ItemIsMortar(pSoldier->inv[BestThrow.bWeaponIn].usItem)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("using mortar, check room to deploy"), gLogDecideActionRed); + UINT8 ubOpponentDir = AIDirection(pSoldier->sGridNo, BestThrow.sTarget); + + // Get new gridno! + INT32 sCheckGridNo = NewGridNo(pSoldier->sGridNo, DirectionInc(ubOpponentDir)); + + if (!OKFallDirection(pSoldier, sCheckGridNo, pSoldier->pathing.bLevel, ubOpponentDir, pSoldier->usAnimState)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("no room to deploy mortar, check if we can move behind"), gLogDecideActionRed); + + // can't fire! + BestThrow.ubPossible = FALSE; + + // try behind us, see if there's room to move back + sCheckGridNo = NewGridNo(pSoldier->sGridNo, DirectionInc(gOppositeDirection[ubOpponentDir])); + if (OKFallDirection(pSoldier, sCheckGridNo, pSoldier->pathing.bLevel, gOppositeDirection[ubOpponentDir], pSoldier->usAnimState)) + { + // sevenfm: check if we can reach this gridno + INT32 iPathCost = EstimatePlotPath(pSoldier, sCheckGridNo, FALSE, FALSE, FALSE, DetermineMovementMode(pSoldier, AI_ACTION_GET_CLOSER), pSoldier->bStealthMode, FALSE, 0); + if (iPathCost != 0 && iPathCost + BestThrow.ubAPCost + GetAPsToLook(pSoldier) + GetAPsCrouch(pSoldier, FALSE) <= pSoldier->bActionPoints) + { + DebugAI(AI_MSG_INFO, pSoldier, String("moving backwards to have more room to deploy mortar"), gLogDecideActionRed); + pSoldier->aiData.usActionData = sCheckGridNo; + + DebugAI(AI_MSG_INFO, pSoldier, String("prepare next action throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime), gLogDecideActionRed); + + // if necessary, swap the usItem + if (BestThrow.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket"), gLogDecideActionRed); + RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); + } + + pSoldier->aiData.usNextActionData = BestThrow.sTarget; + pSoldier->aiData.bNextTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + + pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; + + return AI_ACTION_GET_CLOSER; + } + } + + // can't fire! + BestThrow.ubPossible = FALSE; + } + } + + // if still possible + if (BestThrow.ubPossible) + { + DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime), gLogDecideActionRed); + + // if necessary, swap the usItem + if (BestThrow.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket"), gLogDecideActionRed); + RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); + } + + // sevenfm: correctly set weapon mode for attached GL + if (IsGrenadeLauncherAttached(&pSoldier->inv[HANDPOS])) + { + DebugAI(AI_MSG_INFO, pSoldier, String("set attached GL mode"), gLogDecideActionRed); + pSoldier->bWeaponMode = WM_ATTACHED_GL; + } + + // stand up before throwing if needed + if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && + pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before throw"), gLogDecideActionRed); + pSoldier->aiData.usActionData = BestThrow.ubStance; + pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; + pSoldier->aiData.usNextActionData = BestThrow.sTarget; + pSoldier->aiData.bNextTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + return AI_ACTION_CHANGE_STANCE; + } + else + { + pSoldier->aiData.usActionData = BestThrow.sTarget; + pSoldier->bTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + } + + DebugAI(AI_MSG_INFO, pSoldier, String("Throw grenade / use launcher!"), gLogDecideActionRed); + return(AI_ACTION_TOSS_PROJECTILE); + } + } + } + else // toss/throw/launch not possible + { + DebugAI(AI_MSG_INFO, pSoldier, String("throw not possible"), gLogDecideActionRed); + // WDS - Fix problem when there is no "best thrown" weapon (i.e., BestThrow.bWeaponIn == NO_SLOT) + // if this dude has a longe-range weapon on him (longer than normal + // sight range), and there's at least one other team-mate around, and + // spotters haven't already been called for, then DO SO! + + if ((BestThrow.bWeaponIn != NO_SLOT) && + (CalcMaxTossRange(pSoldier, pSoldier->inv[BestThrow.bWeaponIn].usItem, TRUE) > MaxNormalDistanceVisible()) && + (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1) && + (gTacticalStatus.ubSpottersCalledForBy == NOBODY)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("throw not possible, call for spotters!"), gLogDecideActionRed); + + // then call for spotters! Uses up the rest of his turn (whatever + // that may be), but from now on, BLACK AI NPC may radio sightings! + gTacticalStatus.ubSpottersCalledForBy = pSoldier->ubID; + + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); + } + } + + + + //////////////////////////////////////////////////////////////////////// + // SNIPER / SUPPRESSION + //////////////////////////////////////////////////////////////////////// + // sevenfm: moved can attack check here as only sniper/suppression code needs usable gun + if (CanNPCAttack(pSoldier) == TRUE) + { + // SNIPER! + // sevenfm: set bAimShotLocation + pSoldier->bAimShotLocation = AIM_SHOT_RANDOM; + CheckIfShotPossible(pSoldier, &BestShot); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: is sniper shot possible? = %d, CTH = %d", BestShot.ubPossible, BestShot.ubChanceToReallyHit)); + DebugAI(AI_MSG_INFO, pSoldier, String("Is sniper shot possible? = %d, CTH = %d", BestShot.ubPossible, BestShot.ubChanceToReallyHit), gLogDecideActionRed); + + if (BestShot.ubPossible && BestShot.ubChanceToReallyHit > 50) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Sniper shot possible!"), gLogDecideActionRed); + // then do it! The functions have already made sure that we have a + // pair of worthy opponents, etc., so we're not just wasting our time + + // if necessary, swap the usItem from holster into the hand position + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: sniper shot possible!"); + if (BestShot.bWeaponIn != HANDPOS) + RearrangePocket(pSoldier, HANDPOS, BestShot.bWeaponIn, FOREVER); + + pSoldier->aiData.usActionData = BestShot.sTarget; + //POSSIBLE STRUCTURE CHANGE PROBLEM. GOTTHARD 7/14/08 + pSoldier->aiData.bAimTime = BestShot.ubAimTime; + pSoldier->bScopeMode = BestShot.bScopeMode; + // check if using sniper rifle + if (Weapon[Item[pSoldier->inv[HANDPOS].usItem].ubClassIndex].ubWeaponType == GUN_SN_RIFLE) + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, New113Message[MSG113_SNIPER]); + return(AI_ACTION_FIRE_GUN); + } + else // snipe not possible + { + DebugAI(AI_MSG_INFO, pSoldier, String("Sniper shot NOT possible!"), gLogDecideActionRed); + // if this dude has a long-range weapon on him (longer than normal + // sight range), and there's at least one other team-mate around, and + // spotters haven't already been called for, then DO SO! + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: sniper shot not possible"); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: weapon in slot #%d", BestShot.bWeaponIn)); + // WDS - Fix problem when there is no "best shot" weapon (i.e., BestShot.bWeaponIn == NO_SLOT) + if (BestShot.bWeaponIn != NO_SLOT) { + OBJECTTYPE* gun = &pSoldier->inv[BestShot.bWeaponIn]; + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: men in sector %d, ubspotters called by %d, nobody %d", gTacticalStatus.Team[pSoldier->bTeam].bMenInSector, gTacticalStatus.ubSpottersCalledForBy, NOBODY)); + if (((IsScoped(gun) && GunRange(gun, pSoldier) > MaxNormalDistanceVisible()) || pSoldier->aiData.bOrders == SNIPER) && // SANDRO - added argument + (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1) && + (gTacticalStatus.ubSpottersCalledForBy == NOBODY)) + + { + // then call for spotters! Uses up the rest of his turn (whatever + // that may be), but from now on, BLACK AI NPC may radio sightings! + gTacticalStatus.ubSpottersCalledForBy = pSoldier->ubID; + // HEADROCK HAM 3.1: This may be causing problems with HAM's lowered AP limit. From now on, we'll check + // whether the soldier has more than 0 APs to begin with. + if (pSoldier->bActionPoints > 0) + pSoldier->bActionPoints = 0; + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: calling for sniper spotters"); + DebugAI(AI_MSG_INFO, pSoldier, String("Call for spotters"), gLogDecideActionRed); + + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); + } + } + } + + //SUPPRESSION FIRE + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Suppression decisions]"), gLogDecideActionRed); + + //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 && + pSoldier->bActionPoints > APBPConstants[AP_MINIMUM] && + IsGunAutofireCapable(&pSoldier->inv[BestShot.bWeaponIn]) && + Weapon[pSoldier->inv[BestShot.bWeaponIn].usItem].swapClips && + (!pSoldier->aiData.bUnderFire && !GuySawEnemy(pSoldier, SEEN_LAST_TURN) && (TileIsOutOfBounds(sClosestOpponent) || PythSpacesAway(pSoldier->sGridNo, sClosestOpponent) > TACTICAL_RANGE / 2) || AICheckIsMachinegunner(pSoldier) && Chance(25) || Chance(10)) && + pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft < gGameExternalOptions.ubAISuppressionMinimumAmmo && + GetMagSize(&pSoldier->inv[BestShot.bWeaponIn]) >= gGameExternalOptions.ubAISuppressionMinimumMagSize) + // || pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft < (UINT8)(GetMagSize(&pSoldier->inv[BestShot.bWeaponIn]) / 4))) + { + // HEADROCK HAM 5: Fixed an issue where no ammo was found, leading to a crash when overloading the + // inventory vector (bAmmoSlot = -1...) + INT8 bAmmoSlot = FindAmmoToReload(pSoldier, BestShot.bWeaponIn, NO_SLOT); + if (bAmmoSlot > -1) + { + OBJECTTYPE* pAmmo = &(pSoldier->inv[bAmmoSlot]); + if ((*pAmmo)[0]->data.ubShotsLeft > pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft && GetAPsToReloadGunWithAmmo(pSoldier, &(pSoldier->inv[BestShot.bWeaponIn]), pAmmo) <= (INT16)pSoldier->bActionPoints) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Reload weapon"), gLogDecideActionRed); + pSoldier->aiData.usActionData = BestShot.bWeaponIn; + return AI_ACTION_RELOAD_GUN; + } + } + } + + // 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) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Found spare ammo"), gLogDecideActionRed); + fExtraClip = TRUE; + } + } + + // CHRISL: Changed from a simple flag to two externalized values for more modder control over AI suppression + // WarmSteel - Don't *always* try to suppress when under 50 CTH + if (BestShot.ubPossible && + BestShot.bWeaponIn != -1 && + // check valid target + !TileIsOutOfBounds(BestShot.sTarget) && + BestShot.ubOpponent != NOBODY && + Chance(100 - BestShot.ubOpponent->ShockLevelPercent() / 2) && + // check weapon/ammo requirements + IsGunAutofireCapable(&pSoldier->inv[BestShot.bWeaponIn]) && + GetMagSize(&pSoldier->inv[BestShot.bWeaponIn]) >= gGameExternalOptions.ubAISuppressionMinimumMagSize && + pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft >= gGameExternalOptions.ubAISuppressionMinimumAmmo && + // check soldier and weapon + pSoldier->aiData.bOrders != SNIPER && + BestShot.ubFriendlyFireChance <= MIN_CHANCE_TO_ACCIDENTALLY_HIT_SOMEONE && + !AICheckIsFlanking(pSoldier) && + (Chance(BestShot.ubChanceToReallyHit) || Chance(gGameExternalOptions.sSuppressionEffectiveness)) && + (!gGameExternalOptions.fAISafeSuppression || CheckSuppressionDirection(pSoldier, BestShot.sTarget, BestShot.bTargetLevel)) && + !pSoldier->RetreatCounterValue() && + // check cover + (fAnyCover || // safe position + !fCanBeSeen && NightLight() && CountFriendsFlankSameSpot(pSoldier, sClosestOpponent) && Chance(50) || + ARMED_VEHICLE(pSoldier) || // tanks don't need cover + ENEMYROBOT(pSoldier) || // robots don't try to be in cover + pSoldier->aiData.bUnderFire && (pSoldier->ubPreviousAttackerID == BestShot.ubOpponent || pSoldier->ubNextToPreviousAttackerID == BestShot.ubOpponent || BestShot.ubOpponent->sLastTarget == pSoldier->sGridNo) || // return fire + Chance((BestShot.ubChanceToReallyHit + 100) / 2) || // 50% chance to fire without cover + //SoldierToSoldierLineOfSightTest(pSoldier, MercPtrs[BestShot.ubOpponent], TRUE, CALC_FROM_ALL_DIRS)) && // can see target after turning + LOS_Raised(pSoldier, BestShot.ubOpponent, CALC_FROM_ALL_DIRS)) && // can see target after turning + // reduce chance to shoot if target is beyond weapon range + (AICheckIsMachinegunner(pSoldier) || + ARMED_VEHICLE(pSoldier) || + ENEMYROBOT(pSoldier) || + AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || + pSoldier->aiData.bUnderFire && (pSoldier->ubPreviousAttackerID == BestShot.ubOpponent || pSoldier->ubNextToPreviousAttackerID == BestShot.ubOpponent || BestShot.ubOpponent->sLastTarget == pSoldier->sGridNo) || // return fire + Chance(100 * (GunRange(&pSoldier->inv[BestShot.bWeaponIn], pSoldier) / CELL_X_SIZE) / PythSpacesAway(pSoldier->sGridNo, BestShot.sTarget))) && + // check that we have spare ammo + (fExtraClip || pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft >= gGameExternalOptions.ubAISuppressionMinimumMagSize)) + { + // then do it! + + // if necessary, swap the usItem from holster into the hand position + DebugAI(AI_MSG_INFO, pSoldier, String("suppression fire possible! target %d level %d aim %d", BestShot.sTarget, BestShot.bTargetLevel, BestShot.ubAimTime), gLogDecideActionRed); + + if (BestShot.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket"), gLogDecideActionRed); + RearrangePocket(pSoldier, HANDPOS, BestShot.bWeaponIn, FOREVER); + } + + pSoldier->bTargetLevel = BestShot.bTargetLevel; + pSoldier->aiData.bAimTime = BestShot.ubAimTime; + pSoldier->bDoAutofire = 0; + pSoldier->bDoBurst = 1; + pSoldier->bScopeMode = BestShot.bScopeMode; + + INT16 ubBurstAPs = 0; + FLOAT dTotalRecoil = 0; + INT32 sActualAimAP; + UINT8 ubAutoPenalty; + INT16 sReserveAP = GetAPsProne(pSoldier, TRUE); + UINT8 ubMinAuto = 5; + + if (BestShot.ubAimTime > 0 && + !UsingNewCTHSystem() && + Chance((100 - BestShot.ubChanceToReallyHit) * (100 - BestShot.ubChanceToReallyHit) / 100)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("set ubAimTime = 0 for OCTH suppression"), gLogDecideActionRed); + BestShot.ubAimTime = 0; + } + + // reserve APs to hide if no cover or enemy is close + if (!AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || PythSpacesAway(pSoldier->sGridNo, BestShot.sTarget) < TACTICAL_RANGE / 2) + { + sReserveAP = APBPConstants[AP_MINIMUM] / 2; + } + if (PythSpacesAway(pSoldier->sGridNo, BestShot.sTarget) > TACTICAL_RANGE || AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || pSoldier->aiData.bUnderFire) + { + ubMinAuto *= 2; + } + + sActualAimAP = CalcAPCostForAiming(pSoldier, BestShot.sTarget, (INT8)pSoldier->aiData.bAimTime); + + if (UsingNewCTHSystem() == true) + { + do + { + pSoldier->bDoAutofire++; + dTotalRecoil += AICalcRecoilForShot(pSoldier, &(pSoldier->inv[BestShot.bWeaponIn]), pSoldier->bDoAutofire); + ubBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &(pSoldier->inv[BestShot.bWeaponIn]), pSoldier->bDoAutofire, pSoldier); + } while (pSoldier->bActionPoints >= BestShot.ubAPCost + sActualAimAP + ubBurstAPs + sReserveAP && + pSoldier->inv[pSoldier->ubAttackingHand][0]->data.gun.ubGunShotsLeft >= pSoldier->bDoAutofire && + pSoldier->bDoAutofire <= 30 && + (dTotalRecoil <= 20.0f || pSoldier->bDoAutofire < ubMinAuto)); + } + else + { + ubAutoPenalty = GetAutoPenalty(&pSoldier->inv[pSoldier->ubAttackingHand], gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_PRONE); + do + { + pSoldier->bDoAutofire++; + ubBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &(pSoldier->inv[BestShot.bWeaponIn]), pSoldier->bDoAutofire, pSoldier); + } while (pSoldier->bActionPoints >= BestShot.ubAPCost + sActualAimAP + ubBurstAPs + sReserveAP && + pSoldier->inv[pSoldier->ubAttackingHand][0]->data.gun.ubGunShotsLeft >= pSoldier->bDoAutofire && + pSoldier->bDoAutofire <= 30 && + (ubAutoPenalty * pSoldier->bDoAutofire <= 80 || pSoldier->bDoAutofire < ubMinAuto)); + } + + pSoldier->bDoAutofire--; + + // Make sure we decided to fire at least one shot! + ubBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &(pSoldier->inv[BestShot.bWeaponIn]), pSoldier->bDoAutofire, pSoldier); + DebugAI(AI_MSG_INFO, pSoldier, String("autofire shots %d APcost %d burst AP %d aimtime %d reserve AP %d", pSoldier->bDoAutofire, BestShot.ubAPCost, ubBurstAPs, sActualAimAP, sReserveAP), gLogDecideActionRed); + + // minimum 3 bullets + if (pSoldier->bDoAutofire >= 3 && pSoldier->bActionPoints >= BestShot.ubAPCost + sActualAimAP + ubBurstAPs + sReserveAP) + { + if (gAnimControl[pSoldier->usAnimState].ubEndHeight != BestShot.ubStance && + IsValidStance(pSoldier, BestShot.ubStance)) + { + pSoldier->aiData.bNextAction = AI_ACTION_FIRE_GUN; + pSoldier->aiData.usNextActionData = BestShot.sTarget; + pSoldier->aiData.bNextTargetLevel = BestShot.bTargetLevel; + pSoldier->aiData.usActionData = BestShot.ubStance; + + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before shooting"), gLogDecideActionRed); + + // show "suppression fire" message only if opponent cannot be seen after turning + if (!LOS_Raised(pSoldier, BestShot.ubOpponent, CALC_FROM_ALL_DIRS)) + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, New113Message[MSG113_SUPPRESSIONFIRE]); + + return(AI_ACTION_CHANGE_STANCE); + } + else + { + pSoldier->aiData.usActionData = BestShot.sTarget; + + // show "suppression fire" message only if opponent cannot be seen after turning + if (!LOS_Raised(pSoldier, BestShot.ubOpponent, CALC_FROM_ALL_DIRS)) + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, New113Message[MSG113_SUPPRESSIONFIRE]); + + DebugAI(AI_MSG_INFO, pSoldier, String("Suppression fire!"), gLogDecideActionRed); + return(AI_ACTION_FIRE_GUN); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Suppression not possible"), gLogDecideActionRed); + pSoldier->bDoBurst = 0; + pSoldier->bDoAutofire = 0; + } + } + } + // suppression not possible, do something else + + + //////////////////////////////////////////////////////////////////////////// + // RADIO OPERATOR TRAIT + //////////////////////////////////////////////////////////////////////////// + decision = DecideActionRadioOperator(pSoldier, gLogDecideActionRed); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + } + + + if (gGameExternalOptions.bNewTacticalAIBehavior) + { + //////////////////////////////////////////////////////////////////////////// + // IF YOU SEE CAPTURED FRIENDS, FREE THEM! + //////////////////////////////////////////////////////////////////////////// + + // Flugente: if we see one of our buddies captured, it is a clear sign of enemy activity! + if (gGameExternalOptions.fAllowPrisonerSystem && pSoldier->bTeam == ENEMY_TEAM) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Free friendly POWs]"), gLogDecideActionRed); + SoldierID ubPerson = GetClosestFlaggedSoldierID(pSoldier, 20, ENEMY_TEAM, SOLDIER_POW, TRUE); + + if (ubPerson != NOBODY) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Found friendly POW"), gLogDecideActionRed); + + // if we are close, we can release this guy + // possible only if not handcuffed (binders can be opened, handcuffs not) + if (!HasItemFlag(ubPerson->inv[HANDPOS].usItem, HANDCUFFS)) + { + if (PythSpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) < 2) + { + DebugAI(AI_MSG_INFO, pSoldier, String("I am close enough to free POW"), gLogDecideActionRed); + + // see if we are facing this person + UINT8 ubDesiredMercDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, ubPerson->sGridNo); + + // if not already facing in that direction, + if (pSoldier->ubDirection != ubDesiredMercDir) + { + pSoldier->aiData.usActionData = ubDesiredMercDir; + + DebugAI(AI_MSG_INFO, pSoldier, String("Change facing"), gLogDecideActionRed); + return(AI_ACTION_CHANGE_FACING); + } + + DebugAI(AI_MSG_INFO, pSoldier, String("Free POW"), gLogDecideActionRed); + return(AI_ACTION_FREE_PRISONER); + } + else + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Move closer to POW"), gLogDecideActionRed); + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // PROTECT VIP + //////////////////////////////////////////////////////////////////////////// + // are we a bodyguard? + if (pSoldier->usSoldierFlagMask & SOLDIER_BODYGUARD) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Bodyguard]"), gLogDecideActionRed); + // is VIP still alive? + SoldierID ubPerson = GetClosestFlaggedSoldierID(pSoldier, 100, pSoldier->bTeam, SOLDIER_VIP, FALSE); + + if (ubPerson != NOBODY) + { + DebugAI(AI_MSG_INFO, pSoldier, String("VIP found"), gLogDecideActionRed); + // we want to stay close to him, but still be able to function properly... stay withing a 7-tile radius + if (SpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) > 7) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Attempt to get close "), gLogDecideActionRed); + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek VIP"), gLogDecideActionRed); + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + } + + + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: calculate morale"); + // calculate our morale + pSoldier->aiData.bAIMorale = MORALE_FEARLESS; // It doesn't feel pity or remorse, or pain, and it absolutely will not stop, EVER! Until you are dead + + + //////////////////////////////////////////////////////////////////////////// + // RADIO RED ALERT: determine %chance to call others and report contact + //////////////////////////////////////////////////////////////////////////// + if (!bInDeepWater) + { + auto decision = DecideActionRadioRedAlert(pSoldier, gLogDecideActionRed); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + } + + + //////////////////////////////////////////////////////////////////////////// + // THROW A SMOKE GRENADE FOR COVER + //////////////////////////////////////////////////////////////////////////// + if (gfTurnBasedAI && + pSoldier->bActionPoints == pSoldier->bInitialActionPoints && + pSoldier->aiData.bUnderFire && + !InARoom(pSoldier->sGridNo, NULL) && + !InSmoke(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + RangeChangeDesire(pSoldier) <= 2 && + (!NightLight() || InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel)) && + !TileIsOutOfBounds(sClosestOpponent) && + PythSpacesAway(pSoldier->sGridNo, sClosestOpponent) > TACTICAL_RANGE / 4 && + (!fProneSightCover && !AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || pSoldier->TakenLargeHit()) && + (pSoldier->TakenLargeHit() || pSoldier->ShockLevelPercent() > 20 + Random(80))) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Self smoke when under fire]"), gLogDecideActionRed); + CheckTossSelfSmoke(pSoldier, &BestThrow); + + if (BestThrow.ubPossible) + { + DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime), gLogDecideActionRed); + + // start retreating for several turns + pSoldier->RetreatCounterStart(2); + + // if necessary, swap the usItem from holster into the hand position + if (BestThrow.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket"), gLogDecideActionRed); + RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); + } + + // stand up before throwing if needed + if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && + pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before throw"), gLogDecideActionRed); + pSoldier->aiData.usActionData = BestThrow.ubStance; + pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; + pSoldier->aiData.usNextActionData = BestThrow.sTarget; + pSoldier->aiData.bNextTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + return AI_ACTION_CHANGE_STANCE; + } + else + { + pSoldier->aiData.usActionData = BestThrow.sTarget; + pSoldier->bTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + } + + DebugAI(AI_MSG_INFO, pSoldier, String("Throw smoke!"), gLogDecideActionRed); + return(AI_ACTION_TOSS_PROJECTILE); + } + else { DebugAI(AI_MSG_INFO, pSoldier, String("Throw not possible"), gLogDecideActionRed); } + } + + + if ((gGameExternalOptions.fEnemyTanksCanMoveInTactical || !ARMED_VEHICLE(pSoldier)) && + !(pSoldier->flags.uiStatusFlags & (SOLDIER_DRIVER | SOLDIER_PASSENGER))) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: main red ai"); + + + //////////////////////////////////////////////////////////////////////////// + // AVOID LIGHT IF SPOT IS DANGEROUS AND NO FRIENDS SEE MY CLOSEST ENEMY + //////////////////////////////////////////////////////////////////////////// + if (ubCanMove && + InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + pSoldier->aiData.bOrders != STATIONARY && + pSoldier->aiData.bOrders != SNIPER && + CountFriendsBlack(pSoldier) == 0) + { + pSoldier->aiData.usActionData = FindNearbyDarkerSpot(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + // move as if leaving water or gas + DebugAI(AI_MSG_INFO, pSoldier, String("Move out of light"), gLogDecideActionRed); + return(AI_ACTION_LEAVE_WATER_GAS); + } + } + + //////////////////////////////////////////////////////////////////////////// + // MAIN RED AI: Decide soldier's preference between SEEKING,HELPING & HIDING + //////////////////////////////////////////////////////////////////////////// + + // get the location of the closest reachable opponent + BOOLEAN fClimb; + INT32 sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb); + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: check to continue flanking"); + // continue flanking + INT32 sFlankGridNo; + + if (TileIsOutOfBounds(sClosestDisturbance)) + sFlankGridNo = pSoldier->lastFlankSpot; + else + sFlankGridNo = sClosestDisturbance; + + // continue flanking + // sevenfm: dont' flank when under fire + if (pSoldier->numFlanks > 0 && + pSoldier->numFlanks < MAX_FLANKS_RED && + gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_PRONE && + !pSoldier->aiData.bUnderFire) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Continue flanking]"), gLogDecideActionRed); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: continue flanking"); + INT16 currDir = GetDirectionFromGridNo(sFlankGridNo, pSoldier); + INT16 origDir = pSoldier->origDir; + pSoldier->numFlanks += 1; + if (pSoldier->flags.lastFlankLeft) + { + if (origDir > currDir) + origDir -= NUM_WORLD_DIRECTIONS; + + // stop flanking condition + if ((currDir - origDir) >= MinFlankDirections(pSoldier)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking, left"), gLogDecideActionRed); + pSoldier->numFlanks = MAX_FLANKS_RED; + } + else + { + pSoldier->aiData.usActionData = FindFlankingSpot(pSoldier, sFlankGridNo, AI_ACTION_FLANK_LEFT); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) //&& (currDir - origDir) < 2 ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Flank left"), gLogDecideActionRed); + return AI_ACTION_FLANK_LEFT; + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking left, tile out of bounds"), gLogDecideActionRed); + pSoldier->numFlanks = MAX_FLANKS_RED; + } + } + } + else + { + if (origDir < currDir) + origDir += NUM_WORLD_DIRECTIONS; + + // stop flanking condition + if ((origDir - currDir) >= MinFlankDirections(pSoldier)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking, right"), gLogDecideActionRed); + pSoldier->numFlanks = MAX_FLANKS_RED; + } + else + { + pSoldier->aiData.usActionData = FindFlankingSpot(pSoldier, sFlankGridNo, AI_ACTION_FLANK_RIGHT); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))//&& (origDir - currDir) < 2 ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Flank right"), gLogDecideActionRed); + return AI_ACTION_FLANK_RIGHT; + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking right, tile ouf of bounds"), gLogDecideActionRed); + pSoldier->numFlanks = MAX_FLANKS_RED; + } + } + } + } + + // 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"); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Stop flanking]"), gLogDecideActionRed); + + // start end flank approach with full APs + if (gfTurnBasedAI && pSoldier->bActionPoints < pSoldier->bInitialActionPoints) + { + DebugAI(AI_MSG_INFO, pSoldier, String("AP not full, wait a turn"), gLogDecideActionRed); + return(AI_ACTION_END_TURN); + } + + if (!TileIsOutOfBounds(sFlankGridNo) && + !GuySawEnemy(pSoldier) && + !pSoldier->aiData.bUnderFire && + !Water(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + pSoldier->bInitialActionPoints >= APBPConstants[AP_MINIMUM] && + (PythSpacesAway(pSoldier->sGridNo, sFlankGridNo) > MIN_FLANK_DIST_RED || + !LocationToLocationLineOfSightTest(pSoldier->sGridNo, pSoldier->pathing.bLevel, sFlankGridNo, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS))) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Move towards enemy"), gLogDecideActionRed); + + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sFlankGridNo, 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, pSoldier->pathing.bLevel) && + !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, sFlankGridNo, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS) && + !LocationToLocationLineOfSightTest(pSoldier->sGridNo, pSoldier->pathing.bLevel, sFlankGridNo, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Can be seen in new position, prepare crouch & shot"), gLogDecideActionRed); + + // reserve APs for a possible crouch plus a shot + INT32 sCautiousGridNo = InternalGoAsFarAsPossibleTowards(pSoldier, sFlankGridNo, (INT8)(MinAPsToAttack(pSoldier, sFlankGridNo, ADDTURNCOST, 0) + GetAPsCrouch(pSoldier, TRUE) + GetAPsToLook(pSoldier)), AI_ACTION_SEEK_OPPONENT, FLAG_CAUTIOUS); + + if (!TileIsOutOfBounds(sCautiousGridNo)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy to cautiosgridno %d", sCautiousGridNo), gLogDecideActionRed); + pSoldier->aiData.usActionData = sCautiousGridNo; + pSoldier->aiData.fAIFlags |= AI_CAUTIOUS; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_SEEK_OPPONENT); + } + + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy to gridno %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + return(AI_ACTION_SEEK_OPPONENT); + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy"), gLogDecideActionRed); + return(AI_ACTION_SEEK_OPPONENT); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Can't advance, stop flanking"), gLogDecideActionRed); + // if we cannot advance to spot, stop trying + pSoldier->numFlanks++; + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking"), gLogDecideActionRed); + // stop + pSoldier->numFlanks++; + } + } + + if (pSoldier->CheckInitialAP() && + pSoldier->bActionPoints >= APBPConstants[AP_MINIMUM] && + gfTurnBasedAI && + pSoldier->pathing.bLevel == 0 && + !pSoldier->aiData.bUnderFire && + !InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + SightCoverAtSpot(pSoldier, pSoldier->sGridNo, TRUE) && + !GuySawEnemy(pSoldier) && + !TileIsOutOfBounds(sClosestDisturbance) && + //!fSeekClimb && + PythSpacesAway(pSoldier->sGridNo, sClosestDisturbance) < TACTICAL_RANGE && + (pSoldier->aiData.bOrders == STATIONARY || pSoldier->aiData.bOrders == SNIPER || RangeChangeDesire(pSoldier) < 4) && + !SoldierToVirtualSoldierLineOfSightTest(pSoldier, sClosestDisturbance, pSoldier->pathing.bLevel, ANIM_STAND, TRUE, CALC_FROM_ALL_DIRS) && + CountFriendsBlack(pSoldier, sClosestDisturbance) == 0) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Set watched location]"), gLogDecideActionRed); + gubNPCAPBudget = 0; + gubNPCDistLimit = 0; + + // check path to closest disturbance and find the point where enemy will appear in sight + if (FindBestPath(pSoldier, sClosestDisturbance, pSoldier->pathing.bLevel, RUNNING, COPYROUTE, PATH_IGNORE_PERSON_AT_DEST | PATH_THROUGH_PEOPLE)) + { + INT16 sLoop; + INT32 sLastSeenSpot = NOWHERE; + + DebugAI(AI_MSG_INFO, pSoldier, String("found path to %d, path size %d ", sClosestDisturbance, pSoldier->pathing.usPathDataSize), gLogDecideActionRed); + DebugAI(AI_MSG_INFO, pSoldier, String("check path for seen spots"), gLogDecideActionRed); + + INT32 sCheckGridNo = pSoldier->sGridNo; + + for (sLoop = pSoldier->pathing.usPathIndex; sLoop < pSoldier->pathing.usPathDataSize; sLoop++) + { + sCheckGridNo = NewGridNo(sCheckGridNo, DirectionInc((UINT8)(pSoldier->pathing.usPathingData[sLoop]))); + + if (SoldierToVirtualSoldierLineOfSightTest(pSoldier, sCheckGridNo, pSoldier->pathing.bLevel, ANIM_STAND, TRUE, CALC_FROM_ALL_DIRS)) + { + sLastSeenSpot = sCheckGridNo; + } + } + + // if found last seen spot + if (!TileIsOutOfBounds(sLastSeenSpot)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("last seen spot %d level %d", sLastSeenSpot, pSoldier->pathing.bLevel), gLogDecideActionRed); + IncrementWatchedLoc(pSoldier->ubID, sLastSeenSpot, pSoldier->pathing.bLevel); + } + } + gubNPCAPBudget = 0; + } + + // if we can move at least 1 square's worth + // and have more APs than we want to reserve + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: can we move? = %d, APs = %d", ubCanMove, pSoldier->bActionPoints)); + + if (ubCanMove && pSoldier->bActionPoints > APBPConstants[MAX_AP_CARRIED]) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: checking hide/seek/help/watch points... orders = %d, attitude = %d", pSoldier->aiData.bOrders, pSoldier->aiData.bAttitude)); + DebugAI(AI_MSG_INFO, pSoldier, String("checking hide/seek/help/watch points... orders = %d, attitude = %d", pSoldier->aiData.bOrders, pSoldier->aiData.bAttitude), gLogDecideActionRed); + // calculate initial points for watch based on highest watch loc + INT8 bSeekPts = 0, bHelpPts = 0, bHidePts = 0, bWatchPts = 0; + + bWatchPts = GetHighestWatchedLocPoints(pSoldier->ubID); + if (bWatchPts <= 0) + { + // no watching + bWatchPts = -99; + } + + // modify RED movement tendencies according to morale + switch (pSoldier->aiData.bAIMorale) + { + case MORALE_HOPELESS: bSeekPts = -99; bHelpPts = -99; bHidePts += +2; bWatchPts = -99; break; + case MORALE_WORRIED: bSeekPts += -2; bHelpPts += 0; bHidePts += +2; bWatchPts += 1; break; + case MORALE_NORMAL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case MORALE_CONFIDENT: bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += 0; break; + case MORALE_FEARLESS: bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += 0; break; + } + + // modify tendencies according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: bSeekPts += -1; bHelpPts += -1; bHidePts += +1; bWatchPts += +1; break; + case ONGUARD: bSeekPts += -1; bHelpPts += 0; bHidePts += +1; bWatchPts += +1; break; + case CLOSEPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case RNDPTPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case POINTPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case FARPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case ONCALL: bSeekPts += 0; bHelpPts += +1; bHidePts += -1; bWatchPts += 0; break; + case SEEKENEMY: bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += -1; break; + case SNIPER: bSeekPts += -1; bHelpPts += 0; bHidePts += +1; bWatchPts += +1; break; + } + + // modify tendencies according to attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: bSeekPts += -1; bHelpPts += 0; bHidePts += +2; bWatchPts += +1; break; + case BRAVESOLO: bSeekPts += +1; bHelpPts += -1; bHidePts += -1; bWatchPts += -1; break; + case BRAVEAID: bSeekPts += +1; bHelpPts += +1; bHidePts += -1; bWatchPts += -1; break; + case CUNNINGSOLO: bSeekPts += 1; bHelpPts += -1; bHidePts += +1; bWatchPts += 0; break; + case CUNNINGAID: bSeekPts += 1; bHelpPts += +1; bHidePts += +1; bWatchPts += 0; break; + case AGGRESSIVE: bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += 0; break; + case ATTACKSLAYONLY:bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += 0; break; + } + + // sevenfm: snipers and soldiers with scoped guns should decide watch more often + if (AIGunScoped(pSoldier) || AICheckIsSniper(pSoldier)) + { + bWatchPts++; + } + + // sevenfm: disable watching if soldier is under fire or in dangerous place + // don't watch if some friends can see my closest opponent + if (fDangerousSpot || + InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel) || + CountFriendsBlack(pSoldier) > 0) + { + // prefer hiding when in dangerous place + if (bHidePts > -90) + bWatchPts = min(bWatchPts, bHidePts - 1); + else + bWatchPts--; + } + + // sevenfm: don't watch when overcrowded and not in a building + if (!InARoom(pSoldier->sGridNo, NULL)) + { + bWatchPts -= CountNearbyFriends(pSoldier, pSoldier->sGridNo, TACTICAL_RANGE / 8); + } + + // sevenfm: don't help if seen enemy recently or under fire + if (GuySawEnemy(pSoldier) || pSoldier->aiData.bUnderFire) + { + bHelpPts -= 10; + } + + if (pSoldier->RetreatCounterValue() > 0) + { + // no seeking when retreating + bSeekPts = -99; + // no helping when retreating + bHelpPts = -99; + + if (bHidePts > -90) + { + bWatchPts = min(bWatchPts, bHidePts - 1); + } + } + + if (!gfTurnBasedAI) + { + // don't search for cover + bHidePts = -99; + } + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: hide = %d, seek = %d, watch = %d, help = %d", bHidePts, bSeekPts, bWatchPts, bHelpPts)); + DebugAI(AI_MSG_INFO, pSoldier, String("hide = %d, seek = %d, watch = %d, help = %d", bHidePts, bSeekPts, bWatchPts, bHelpPts), gLogDecideActionRed); + // while one of the three main RED REACTIONS remains viable + while ((bSeekPts > -90) || (bHelpPts > -90) || (bHidePts > -90)) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: checking to seek"); + // if SEEKING is possible and at least as desirable as helping or hiding + if (((bSeekPts > -90) && (bSeekPts >= bHelpPts) && (bSeekPts >= bHidePts) && (bSeekPts >= bWatchPts))) + { + // if there is an opponent reachable + // sevenfm: allow seeking in prone stance if we haven't seen enemy for several turns + if (!TileIsOutOfBounds(sClosestDisturbance) && + (gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_PRONE || !GuySawEnemy(pSoldier))) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: seek opponent"); + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy"), gLogDecideActionRed); + ////////////////////////////////////////////////////////////////////// + // SEEK CLOSEST DISTURBANCE: GO DIRECTLY TOWARDS CLOSEST KNOWN OPPONENT + ////////////////////////////////////////////////////////////////////// + + // try to move towards him + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestDisturbance, GetAPsCrouch(pSoldier, TRUE), AI_ACTION_SEEK_OPPONENT, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + // Check for a trap + if (!ArmySeesOpponents()) + { + if (GetNearestRottingCorpseAIWarning(pSoldier->aiData.usActionData) > 0) + { + // abort! abort! + pSoldier->aiData.usActionData = NOWHERE; + } + } + } + + // if it's possible + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + // do it! + sprintf(tempstr, "%s - SEEKING OPPONENT at grid %d, MOVING to %d", + pSoldier->name, sClosestDisturbance, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + if (!ENEMYROBOT(pSoldier) && fClimb)//&& pSoldier->aiData.usActionData == sClosestDisturbance) + { + // need to climb AND have enough APs to get there this turn + BOOLEAN fUp = TRUE; + if (pSoldier->pathing.bLevel > 0) + fUp = FALSE; + + if (!fUp) + DebugMsg(TOPIC_JA2AI, DBG_LEVEL_3, String("Soldier %d is climbing down", pSoldier->ubID)); + + // As mentioned in the next part, the sClosestDisturbance IS the climb point desired. So the + // check here should be "Am I aready there?" If so, THEN possibly climb. This previous check + // would have a soldier climbing any building, even if it was not the desired building. So + // WRONG WRONG WRONG + //if ( CanClimbFromHere ( pSoldier, fUp ) ) + if (pSoldier->sGridNo == sClosestDisturbance) + { + if (IsActionAffordable(pSoldier) && pSoldier->bActionPoints >= (APBPConstants[AP_CLIMBROOF] + MinAPsToAttack(pSoldier, sClosestDisturbance, ADDTURNCOST, 0))) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Climb roof at gridno %d", sClosestDisturbance), gLogDecideActionRed); + return(AI_ACTION_CLIMB_ROOF); + } + } + else + { + // Do not overwrite the usActionData here. If there's no nearby climb point, the action data + // would become NOWHERE, and then the SEEK_ENEMY fallback would also fail. + // In fact, sClosestDisturbance has ALREADY calculated the closest climb point when climbing is + // necessary. The returned grid # in sClosestDisturbance is that climb point. So if climb is + // set, then use sClosestDisturbance as is. + //INT16 usClimbPoint = FindClosestClimbPoint(pSoldier, pSoldier->sGridNo , sClosestDisturbance , fUp ); + INT32 usClimbPoint = sClosestDisturbance; + if (!TileIsOutOfBounds(usClimbPoint)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Move towards climb spot %d", usClimbPoint), gLogDecideActionRed); + pSoldier->aiData.usActionData = usClimbPoint; + return(AI_ACTION_MOVE_TO_CLIMB); + } + } + } + //if ( fClimb && pSoldier->aiData.usActionData == sClosestDisturbance) + //{ + // return( AI_ACTION_CLIMB_ROOF ); + //} + + BOOLEAN fOvercrowded = FALSE; + if (CountNearbyFriends(pSoldier, pSoldier->sGridNo, TACTICAL_RANGE / 4) > 2) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Soldier position %d is overcrowded", pSoldier->sGridNo), gLogDecideActionRed); + fOvercrowded = TRUE; + } + + // sevenfm: possibly start RED flanking + if ((pSoldier->aiData.bAttitude == CUNNINGAID || pSoldier->aiData.bAttitude == CUNNINGSOLO || + (pSoldier->aiData.bAttitude == BRAVESOLO || pSoldier->aiData.bAttitude == BRAVEAID) && fOvercrowded) && + pSoldier->bTeam == ENEMY_TEAM && + gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_PRONE && + !pSoldier->aiData.bUnderFire && + pSoldier->pathing.bLevel == 0 && + (pSoldier->aiData.bOrders == SEEKENEMY || + pSoldier->aiData.bOrders == FARPATROL || + pSoldier->aiData.bOrders == CLOSEPATROL && NightTime()) && + (!GuySawEnemy(pSoldier) || fOvercrowded) && + !Water(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + pSoldier->bActionPoints >= APBPConstants[AP_MINIMUM] && + (CountFriendsInDirection(pSoldier, sClosestDisturbance) > 1 || NightTime() || fOvercrowded)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Possibly start flanking]"), gLogDecideActionRed); + INT8 action = AI_ACTION_SEEK_OPPONENT; + INT16 dist = PythSpacesAway(pSoldier->sGridNo, sClosestDisturbance); + if (dist > MIN_FLANK_DIST_RED && dist < MAX_FLANK_DIST_RED) + { + INT16 rdm = Random(6); + + switch (rdm) + { + case 1: + case 2: + case 3: + if (pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Try to flank left"), gLogDecideActionRed); + action = AI_ACTION_FLANK_LEFT; + } + break; + default: + if (pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Try to flank right"), gLogDecideActionRed); + action = AI_ACTION_FLANK_RIGHT; + } + break; + } + + if (action == AI_ACTION_SEEK_OPPONENT) { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy instead"), gLogDecideActionRed); + return action; + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Distance not suitable, seek enemy instead"), gLogDecideActionRed); + return AI_ACTION_SEEK_OPPONENT; + } + pSoldier->aiData.usActionData = FindFlankingSpot(pSoldier, sClosestDisturbance, action); + + if (TileIsOutOfBounds(pSoldier->aiData.usActionData) || pSoldier->numFlanks >= MAX_FLANKS_RED) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Flanking spot %d out of bounds or numFlanks >= MAX_FLANKS_RED", pSoldier->aiData.usActionData), gLogDecideActionRed); + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestDisturbance, GetAPsCrouch(pSoldier, TRUE), AI_ACTION_SEEK_OPPONENT, 0); + //pSoldier->numFlanks = 0; + if (PythSpacesAway(pSoldier->aiData.usActionData, sClosestDisturbance) < 5 || LocationToLocationLineOfSightTest(pSoldier->aiData.usActionData, pSoldier->pathing.bLevel, sClosestDisturbance, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS)) + { + // reserve APs for a possible crouch plus a shot + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestDisturbance, (INT8)(MinAPsToAttack(pSoldier, sClosestDisturbance, ADDTURNCOST, 0) + GetAPsCrouch(pSoldier, TRUE)), AI_ACTION_SEEK_OPPONENT, FLAG_CAUTIOUS); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Reserved AP for crouch & shot, seek enemy"), gLogDecideActionRed); + pSoldier->aiData.fAIFlags |= AI_CAUTIOUS; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_SEEK_OPPONENT); + } + } + + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy"), gLogDecideActionRed); + return(AI_ACTION_SEEK_OPPONENT); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Found flanking spot %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + if (action == AI_ACTION_FLANK_LEFT) + pSoldier->flags.lastFlankLeft = TRUE; + else + pSoldier->flags.lastFlankLeft = FALSE; + + if (pSoldier->lastFlankSpot != sClosestDisturbance) + pSoldier->numFlanks = 0; + + + pSoldier->origDir = GetDirectionFromGridNo(sClosestDisturbance, pSoldier); + pSoldier->lastFlankSpot = sClosestDisturbance; + pSoldier->numFlanks++; + + // sevenfm: change orders when starting to flank + if (pSoldier->aiData.bOrders == CLOSEPATROL) + { + pSoldier->aiData.bOrders = FARPATROL; + } + + DebugAI(AI_MSG_INFO, pSoldier, String("Start flanking"), gLogDecideActionRed); + return(action); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Not flanking, move up towards enemy"), gLogDecideActionRed); + // let's be a bit cautious about going right up to a location without enough APs to shoot + if (PythSpacesAway(pSoldier->aiData.usActionData, sClosestDisturbance) < 5 || LocationToLocationLineOfSightTest(pSoldier->aiData.usActionData, pSoldier->pathing.bLevel, sClosestDisturbance, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS)) + { + // reserve APs for a possible crouch plus a shot + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestDisturbance, (INT8)(MinAPsToAttack(pSoldier, sClosestDisturbance, ADDTURNCOST, 0) + GetAPsCrouch(pSoldier, TRUE)), AI_ACTION_SEEK_OPPONENT, FLAG_CAUTIOUS); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Reserved AP for crouch & shot, seek enemy"), gLogDecideActionRed); + pSoldier->aiData.fAIFlags |= AI_CAUTIOUS; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_SEEK_OPPONENT); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy"), gLogDecideActionRed); + return(AI_ACTION_SEEK_OPPONENT); + } + break; + } + } + } + + // mark SEEKING as impossible for next time through while loop +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "couldn't SEEK...", 1000); +#endif + bSeekPts = -99; + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: couldn't seek"); + } + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: checking to watch"); + // if WATCHING is possible and at least as desirable as anything else + if ((bWatchPts > -90) && (bWatchPts >= bSeekPts) && (bWatchPts >= bHelpPts) && (bWatchPts >= bHidePts)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("[watch]"), gLogDecideActionRed); + // take a look at our highest watch point... if it's still visible, turn to face it and then wait + INT8 bHighestWatchLoc = GetHighestVisibleWatchedLoc(pSoldier->ubID); + + if (bHighestWatchLoc != -1) + { + // see if we need turn to face that location + UINT8 ubOpponentDir = AIDirection(pSoldier->sGridNo, gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc]); + DebugAI(AI_MSG_INFO, pSoldier, String("Highest watch location: [%d] %d %d watch dir: %d", bHighestWatchLoc, gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc], gbWatchedLocLevel[pSoldier->ubID][bHighestWatchLoc], ubOpponentDir), gLogDecideActionRed); + + // consider at least crouching + if (gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_STAND && + IsValidStance(pSoldier, ANIM_CROUCH) && + pSoldier->bActionPoints >= GetAPsCrouch(pSoldier, TRUE)) + { + pSoldier->aiData.usActionData = ANIM_CROUCH; + + DebugAI(AI_MSG_INFO, pSoldier, String("crouch to watch"), gLogDecideActionRed); + return(AI_ACTION_CHANGE_STANCE); + } + + // raise weapon if not raised + if (PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION && + !WeaponReady(pSoldier) && + (pSoldier->bBreath > OKBREATH * 2 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 50) && + pSoldier->bActionPoints >= GetAPsToReadyWeapon(pSoldier, PickSoldierReadyAnimation(pSoldier, FALSE, FALSE))) + { + DebugAI(AI_MSG_INFO, pSoldier, String("raise weapon"), gLogDecideActionRed); + return AI_ACTION_RAISE_GUN; + } + + // if soldier is not already facing in that direction + if (pSoldier->ubDirection != ubOpponentDir && + pSoldier->InternalIsValidStance(ubOpponentDir, gAnimControl[pSoldier->usAnimState].ubEndHeight) && + pSoldier->bActionPoints >= GetAPsToLook(pSoldier)) + { + // turn + pSoldier->aiData.usActionData = ubOpponentDir; + DebugAI(AI_MSG_INFO, pSoldier, String("turn to watched location"), gLogDecideActionRed); + return(AI_ACTION_CHANGE_FACING); + } + + // possibly go prone, check that we'll have line of sight to standing enemy at watched location + if (gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_CROUCH && + IsValidStance(pSoldier, ANIM_PRONE) && + pSoldier->bActionPoints >= GetAPsProne(pSoldier, TRUE) && + (!InARoom(pSoldier->sGridNo, NULL) || pSoldier->pathing.bLevel > 0 || pSoldier->aiData.bUnderFire) && + gfTurnBasedAI && + LocationToLocationLineOfSightTest(pSoldier->sGridNo, pSoldier->pathing.bLevel, gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc], gbWatchedLocLevel[pSoldier->ubID][bHighestWatchLoc], TRUE, pSoldier->GetMaxDistanceVisible(gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc], gbWatchedLocLevel[pSoldier->ubID][bHighestWatchLoc], CALC_FROM_ALL_DIRS), PRONE_LOS_POS, STANDING_LOS_POS)) + { + pSoldier->aiData.usActionData = ANIM_PRONE; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + DebugAI(AI_MSG_INFO, pSoldier, String("go prone, end turn"), gLogDecideActionRed); + return(AI_ACTION_CHANGE_STANCE); + } + + DebugAI(AI_MSG_INFO, pSoldier, String("watch at %d level %d", gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc], gbWatchedLocLevel[pSoldier->ubID][bHighestWatchLoc]), gLogDecideActionRed); + return(AI_ACTION_NONE); + } + + bWatchPts = -99; + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: couldn't watch"); + } + + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: checking to help"); + // if HELPING is possible and at least as desirable as seeking or hiding + if ((bHelpPts > -90) && (bHelpPts >= bSeekPts) && (bHelpPts >= bHidePts) && (bHelpPts >= bWatchPts)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Help a friend]"), gLogDecideActionRed); +#ifdef AI_TIMING_TESTS + uiStartTime = GetJA2Clock(); +#endif + INT32 sClosestFriend = ClosestReachableFriendInTrouble(pSoldier, &fClimb); +#ifdef AI_TIMING_TESTS + uiEndTime = GetJA2Clock(); + + guiRedHelpTimeTotal += (uiEndTime - uiStartTime); + guiRedHelpCounter++; +#endif + //WarmSteel - Dont try if we're already quite close to our friend + // sevenfm: reverted to vanilla helping + //if (!TileIsOutOfBounds(sClosestFriend) && PythSpacesAway(pSoldier->sGridNo, sClosestFriend) > pSoldier->GetMaxDistanceVisible(sClosestFriend, 0, CALC_FROM_ALL_DIRS )) + if (!TileIsOutOfBounds(sClosestFriend)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Closest friend at gridno %d", sClosestFriend), gLogDecideActionRed); + ////////////////////////////////////////////////////////////////////// + // GO DIRECTLY TOWARDS CLOSEST FRIEND UNDER FIRE OR WHO LAST RADIOED + ////////////////////////////////////////////////////////////////////// + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestFriend, GetAPsCrouch(pSoldier, TRUE), AI_ACTION_SEEK_OPPONENT, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - SEEKING FRIEND at %d, MOVING to %d", + pSoldier->name, sClosestFriend, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + DebugAI(AI_MSG_INFO, pSoldier, String("Seeking friend, moving to %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + + if (!ENEMYROBOT(pSoldier) && fClimb)//&& pSoldier->aiData.usActionData == sClosestFriend) + { + // need to climb AND have enough APs to get there this turn + BOOLEAN fUp = TRUE; + if (pSoldier->pathing.bLevel > 0) + fUp = FALSE; + + if (!fUp) + DebugMsg(TOPIC_JA2AI, DBG_LEVEL_3, String("Soldier %d is climbing down", pSoldier->ubID)); + + // 0verhaul: Yet another chance to climb the wrong building and otherwise waste CPU power. + // We already know the climb point we want, which may not be here even if climbing is possible. + //if ( CanClimbFromHere ( pSoldier, fUp ) ) + if (pSoldier->sGridNo == sClosestFriend) + { + if (IsActionAffordable(pSoldier) && pSoldier->bActionPoints >= (APBPConstants[AP_CLIMBROOF] + MinAPsToAttack(pSoldier, sClosestFriend, ADDTURNCOST, 0))) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Climb roof"), gLogDecideActionRed); + return(AI_ACTION_CLIMB_ROOF); + } + } + else + { + pSoldier->aiData.usActionData = sClosestFriend; + //INT32 sClimbPoint = FindClosestClimbPoint(pSoldier, pSoldier->sGridNo , sClosestFriend , fUp ); + //if (!TileIsOutOfBounds(sClimbPoint)) + { + //pSoldier->aiData.usActionData = sClimbPoint; + DebugAI(AI_MSG_INFO, pSoldier, String("Move towards climb point"), gLogDecideActionRed); + return(AI_ACTION_MOVE_TO_CLIMB); + } + } + } + //if (fClimb && pSoldier->aiData.usActionData == sClosestFriend) + //{ + // return( AI_ACTION_CLIMB_ROOF ); + //} + DebugAI(AI_MSG_INFO, pSoldier, String("Seek friend"), gLogDecideActionRed); + return(AI_ACTION_SEEK_FRIEND); + } + } + + // mark SEEKING as impossible for next time through while loop +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "couldn't HELP...", 1000); +#endif + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: couldn't help"); + bHelpPts = -99; + } + + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: checking to hide"); + // if HIDING is possible and at least as desirable as seeking or helping + if ((bHidePts > -90) && (bHidePts >= bSeekPts) && (bHidePts >= bHelpPts) && (bHidePts >= bWatchPts)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Take cover]"), gLogDecideActionRed); + //sClosestOpponent = ClosestKnownOpponent( pSoldier, NULL, NULL ); + // if an opponent is known (not necessarily reachable or conscious) + if (!SkipCoverCheck && !TileIsOutOfBounds(sClosestOpponent)) + { + ////////////////////////////////////////////////////////////////////// + // TAKE BEST NEARBY COVER FROM ALL KNOWN OPPONENTS + ////////////////////////////////////////////////////////////////////// +#ifdef AI_TIMING_TESTS + uiStartTime = GetJA2Clock(); +#endif + INT32 iDummy; + pSoldier->aiData.usActionData = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iDummy); +#ifdef AI_TIMING_TESTS + uiEndTime = GetJA2Clock(); + + guiRedHideTimeTotal += (uiEndTime - uiStartTime); + guiRedHideCounter++; +#endif + + // let's be a bit cautious about going right up to a location without enough APs to shoot + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Found a cover spot at %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb); + if (!TileIsOutOfBounds(sClosestDisturbance) && (SpacesAway(pSoldier->aiData.usActionData, sClosestDisturbance) < 5 || SpacesAway(pSoldier->aiData.usActionData, sClosestDisturbance) + 5 < SpacesAway(pSoldier->sGridNo, sClosestDisturbance))) + { + // either moving significantly closer or into very close range + // ensure will we have enough APs for a possible crouch plus a shot + if (InternalGoAsFarAsPossibleTowards(pSoldier, pSoldier->aiData.usActionData, (INT8)(MinAPsToAttack(pSoldier, sClosestOpponent, ADDTURNCOST, 0) + GetAPsCrouch(pSoldier, TRUE)), AI_ACTION_TAKE_COVER, 0) == pSoldier->aiData.usActionData) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Moving to cover, reserve AP for crouch & shot"), gLogDecideActionRed); + return(AI_ACTION_TAKE_COVER); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Moving to cover"), gLogDecideActionRed); + return(AI_ACTION_TAKE_COVER); + } + } + + } + + // mark HIDING as impossible for next time through while loop +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "couldn't HIDE...", 1000); +#endif + + bHidePts = -99; + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: couldn't hide"); + } + } + } + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: nothing to do!"); + //////////////////////////////////////////////////////////////////////////// + // NOTHING USEFUL POSSIBLE! IF NPC IS CURRENTLY UNDER FIRE, TRY TO RUN AWAY + //////////////////////////////////////////////////////////////////////////// + + // if we're currently under fire (presumably, attacker is hidden) + if (pSoldier->aiData.bUnderFire) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Under fire]"), gLogDecideActionRed); + + // only try to run if we've actually been hit recently & noticably so + // otherwise, presumably our current cover is pretty good & sufficient + // HEADROCK HAM B2.6: New value here helps us change the ratio of running away due to shock. This + // is terribly important if Suppression Shock is enabled. + UINT16 bShock = 0; + + if (gGameExternalOptions.usSuppressionShockEffect > 0) + { + // If bShock value is greater than (2*ExpLevel + MoraleModifier)*1.5, the target will flee. + bShock = pSoldier->aiData.bShock; + if (bShock <= ((float)CalcSuppressionTolerance(pSoldier) * (float)1.5)) + bShock = 0; + } + else + { + bShock = pSoldier->aiData.bShock; + } + + if (bShock > 0) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Soldier is shocked, attempt to run away"), gLogDecideActionRed); + // look for best place to RUN AWAY to (farthest from the closest threat) + pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s RUNNING AWAY to grid %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: run away!"); + DebugAI(AI_MSG_INFO, pSoldier, String("Running away to gridno %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + return(AI_ACTION_RUN_AWAY); + } + } + + //////////////////////////////////////////////////////////////////////////// + // UNDER FIRE, DON'T WANNA/CAN'T RUN AWAY, SO CROUCH + //////////////////////////////////////////////////////////////////////////// + + DebugAI(AI_MSG_INFO, pSoldier, String("Under fire, try to change stance"), gLogDecideActionRed); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: crouch or go prone"); + // if not in water and not already crouched + if (gAnimControl[pSoldier->usAnimState].ubHeight == ANIM_STAND && IsValidStance(pSoldier, ANIM_CROUCH)) + { + if (!gfTurnBasedAI || GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints) + { + +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s CROUCHES (STATUS RED)", pSoldier->name); + AIPopMessage(tempstr); +#endif + DebugAI(AI_MSG_INFO, pSoldier, String("Crouching"), gLogDecideActionRed); + + pSoldier->aiData.usActionData = ANIM_CROUCH; + return(AI_ACTION_CHANGE_STANCE); + } + } + else if (gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_PRONE) + { + // maybe go prone + if (PreRandom(2) == 0 && IsValidStance(pSoldier, ANIM_PRONE)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Go prone"), gLogDecideActionRed); + pSoldier->aiData.usActionData = ANIM_PRONE; + return(AI_ACTION_CHANGE_STANCE); + } + } + } + } + + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: look around towards opponent"); + //////////////////////////////////////////////////////////////////////////// + // LOOK AROUND TOWARD CLOSEST KNOWN OPPONENT, IF KNOWN + //////////////////////////////////////////////////////////////////////////// + + if (!gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) + { + // determine the location of the known closest opponent + // (don't care if he's conscious, don't care if he's reachable at all) + //sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); + + if (!TileIsOutOfBounds(sClosestOpponent)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Look around towards enemy]"), gLogDecideActionRed); + // determine direction from this soldier to the closest opponent + UINT8 ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestOpponent); + + // if soldier is not already facing in that direction, + // and the opponent is close enough that he could possibly be seen + // note, have to change this to use the level returned from ClosestKnownOpponent + INT32 sDistVisible = pSoldier->GetMaxDistanceVisible(sClosestOpponent, 0, CALC_FROM_ALL_DIRS); + + if ((pSoldier->ubDirection != ubOpponentDir) && (PythSpacesAway(pSoldier->sGridNo, sClosestOpponent) <= sDistVisible)) + { + // set base chance according to orders + INT32 iChance; + if ((pSoldier->aiData.bOrders == STATIONARY) || (pSoldier->aiData.bOrders == ONGUARD)) + iChance = 50; + else // all other orders + iChance = 25; + + if (pSoldier->aiData.bAttitude == DEFENSIVE) + iChance += 25; + + if (ARMED_VEHICLE(pSoldier)) + { + iChance += 50; + } + + if ((INT16)PreRandom(100) < iChance && pSoldier->InternalIsValidStance(ubOpponentDir, gAnimControl[pSoldier->usAnimState].ubEndHeight)) + { + pSoldier->aiData.usActionData = ubOpponentDir; + +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - TURNS TOWARDS CLOSEST ENEMY to face direction %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + DebugAI(AI_MSG_INFO, pSoldier, String("Turn towards closest enemy, face direction %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + if (pSoldier->aiData.bOrders == SNIPER && + !WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION && + (pSoldier->bBreath > 15 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 50)) + { + if (!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, READY_RIFLE_CROUCH) <= pSoldier->bActionPoints) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, sniper"), gLogDecideActionRed); + pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; + } + } + //////////////////////////////////////////////////////////////////////////// + // SANDRO - allow regular soldiers to raise scoped weapons to see rather away too + else if (IsScoped(&pSoldier->inv[HANDPOS])) + { + if (!WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION && + (pSoldier->bBreath > 15 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 50)) + { + if (!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, READY_RIFLE_CROUCH) <= pSoldier->bActionPoints) + { + if (Random(100) < 35) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, scoped weapon"), gLogDecideActionRed); + pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; + } + } + } + } + //////////////////////////////////////////////////////////////////////////// + return(AI_ACTION_CHANGE_FACING); + } + } + //////////////////////////////////////////////////////////////////////////// + // SANDRO - allow regular soldiers to raise scoped weapons to see farther away too + else if (pSoldier->ubDirection == ubOpponentDir && + !WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Facing enemy already"), gLogDecideActionRed); + if ((!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, pSoldier->usAnimState) <= pSoldier->bActionPoints) && (pSoldier->bBreath > 15 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 50)) + { + if (pSoldier->aiData.bOrders == SNIPER) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, sniper"), gLogDecideActionRed); + return AI_ACTION_RAISE_GUN; + } + else if (IsScoped(&pSoldier->inv[HANDPOS])) + { + if (Random(100) < 40) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, scoped weapon"), gLogDecideActionRed); + return AI_ACTION_RAISE_GUN; + } + } + else + { + if (Random(100) < 20) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun"), gLogDecideActionRed); + return AI_ACTION_RAISE_GUN; + } + } + } + } + //////////////////////////////////////////////////////////////////////////// + } + } + + if (ARMED_VEHICLE(pSoldier)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Armed vehicle]"), gLogDecideActionRed); + // try turning in a random direction as we still can't see anyone. + if (!gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) + { + INT32 sClosestDisturbance = MostImportantNoiseHeard(pSoldier, NULL, NULL, NULL); + UINT8 ubOpponentDir; + if (!TileIsOutOfBounds(sClosestDisturbance)) + { + ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestDisturbance); + if (pSoldier->ubDirection == ubOpponentDir) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Already facing closest disturbance, face a random direction"), gLogDecideActionRed); + ubOpponentDir = (UINT8)PreRandom(NUM_WORLD_DIRECTIONS); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Closest disturbance out of bounds, face a random direction"), gLogDecideActionRed); + ubOpponentDir = (UINT8)PreRandom(NUM_WORLD_DIRECTIONS); + } + + if ((pSoldier->ubDirection != ubOpponentDir)) + { + if ((pSoldier->bActionPoints == pSoldier->bInitialActionPoints || (INT16)PreRandom(100) < 60) && pSoldier->InternalIsValidStance(ubOpponentDir, gAnimControl[pSoldier->usAnimState].ubEndHeight)) + { + pSoldier->aiData.usActionData = ubOpponentDir; + +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - TURNS TOWARDS CLOSEST ENEMY to face direction %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + // limit turning a bit... if the last thing we did was also a turn, add a 60% chance of this being our last turn + if (pSoldier->aiData.bLastAction == AI_ACTION_CHANGE_FACING && PreRandom(100) < 60) + { + if (gfTurnBasedAI) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Ending turn to limit facing changes"), gLogDecideActionRed); + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + } + else + { + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = (UINT16)REALTIME_AI_DELAY; + } + } + + DebugAI(AI_MSG_INFO, pSoldier, String("Turn towards closest disturbance, direction %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + return(AI_ACTION_CHANGE_FACING); + } + } + } + + // that's it for tanks + DebugAI(AI_MSG_INFO, pSoldier, String("Do nothing"), gLogDecideActionRed); + return(AI_ACTION_NONE); + } + + //////////////////////////////////////////////////////////////////////////// + // LEAVE THE SECTOR + //////////////////////////////////////////////////////////////////////////// + + // NOT IMPLEMENTED + + + //////////////////////////////////////////////////////////////////////////// + // PICKUP A NEARBY ITEM THAT'S USEFUL + //////////////////////////////////////////////////////////////////////////// + + if (ubCanMove && !pSoldier->aiData.bNeutral && (gfTurnBasedAI || pSoldier->bTeam == ENEMY_TEAM)) + { + pSoldier->aiData.bAction = SearchForItems(pSoldier, SEARCH_GENERAL_ITEMS, pSoldier->inv[HANDPOS].usItem); + + // sevenfm: check that location is safe + if (pSoldier->aiData.bAction != AI_ACTION_NONE && + !TileIsOutOfBounds(pSoldier->aiData.usActionData) && + (GetNearestRottingCorpseAIWarning(pSoldier->aiData.usActionData) > 0 || + InLightAtNight(pSoldier->aiData.usActionData, pSoldier->pathing.bLevel) && !InLightAtNight(pSoldier->aiData.usActionData, pSoldier->pathing.bLevel)) && + !fDangerousSpot && + CountFriendsBlack(pSoldier) == 0) + { + // abort! abort! + DebugAI(AI_MSG_INFO, pSoldier, String("Unsafe location, do nothing"), gLogDecideActionRed); + pSoldier->aiData.bAction = AI_ACTION_NONE; + } + + if (pSoldier->aiData.bAction != AI_ACTION_NONE) + { + return(pSoldier->aiData.bAction); + } + } + + /* JULY 29, 1996 - Decided that this was a bad idea, after watching a civilian + start a random patrol while 2 steps away from a hidden armed opponent...*/ + + //////////////////////////////////////////////////////////////////////////// + // SWITCH TO GREEN: soldier does ordinary regular patrol, seeks friends + //////////////////////////////////////////////////////////////////////////// + + // if not in combat or under fire, and we COULD have moved, just chose not to + BOOLEAN fClimb; + if ((pSoldier->aiData.bAlertStatus != STATUS_BLACK) && !pSoldier->aiData.bUnderFire && ubCanMove && (!gfTurnBasedAI || pSoldier->bActionPoints >= pSoldier->bInitialActionPoints) && (TileIsOutOfBounds(ClosestReachableDisturbance(pSoldier, &fClimb)))) + { + // addition: if soldier is bleeding then reduce bleeding and do nothing + if (pSoldier->bBleeding > MIN_BLEEDING_THRESHOLD) + { + // reduce bleeding by 1 point per AP (in RT, APs will get recalculated so it's okay) + pSoldier->bBleeding = __max(0, pSoldier->bBleeding - (pSoldier->bActionPoints / 2)); + return(AI_ACTION_NONE); // will end-turn/wait depending on whether we're in TB or realtime + } +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "- chose to SKIP all RED actions, BYPASSES to GREEN!", 1000); +#endif + // Skip RED until new situation/next turn, 30% extra chance to do GREEN actions + pSoldier->aiData.bBypassToGreen = 30; + return(DecideActionGreenRobot(pSoldier)); + } + + + //////////////////////////////////////////////////////////////////////////// + // IF UNDER FIRE, FACE THE MOST IMPORTANT NOISE WE KNOW + //////////////////////////////////////////////////////////////////////////// + + if (pSoldier->aiData.bUnderFire && pSoldier->bActionPoints >= (pSoldier->bInitialActionPoints - GetAPsToLook(pSoldier)) && IsValidStance(pSoldier, ANIM_PRONE)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Under fire, go prone]"), gLogDecideActionRed); + INT32 sClosestDisturbance = MostImportantNoiseHeard(pSoldier, NULL, NULL, NULL); + + if (!TileIsOutOfBounds(sClosestDisturbance)) + { + UINT8 ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestDisturbance); + if (pSoldier->ubDirection != ubOpponentDir) + { + if (!gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Face direction %d", ubOpponentDir), gLogDecideActionRed); + pSoldier->aiData.usActionData = ubOpponentDir; + return(AI_ACTION_CHANGE_FACING); + } + } + } + } + + + + //////////////////////////////////////////////////////////////////////////// + // DO NOTHING: Not enough points left to move, so save them for next turn + //////////////////////////////////////////////////////////////////////////// + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionRed: do nothing at all...")); +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "- DOES NOTHING (RED)", 1000); +#endif + DebugAI(AI_MSG_INFO, pSoldier, String("Do nothing"), gLogDecideActionRed); + + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); +} + +INT8 DecideActionBlackRobot(SOLDIERTYPE* pSoldier) +{ + DebugAI(AI_MSG_START, pSoldier, String("[Black Robot]")); + LogDecideInfo(pSoldier); + + INT32 iCoverPercentBetter, iOffense, iDefense, iChance; + ATTACKTYPE BestShot, BestThrow, BestStab, BestAttack; + auto decision = AI_ACTION_INVALID; + + INT32 sOpponentGridNo; + INT8 bOpponentLevel; + INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel); + DebugAI(AI_MSG_INFO, pSoldier, String("sClosestOpponent %d", sClosestOpponent)); + + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // sevenfm: initialize data + pSoldier->bWeaponMode = WM_NORMAL; + + // sevenfm: stop flanking when we see enemy + if (AICheckIsFlanking(pSoldier)) + { + pSoldier->numFlanks = 0; + } + + // if we have absolutely no action points, we can't do a thing under BLACK! + if (pSoldier->bActionPoints <= 0) + { + pSoldier->aiData.usActionData = NOWHERE; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_NONE); + } + + // can this guy move to any of the neighbouring squares ? (sets TRUE/FALSE) + UINT8 ubCanMove = (pSoldier->bActionPoints >= MinPtsToMove(pSoldier)); + + if (pSoldier->flags.uiStatusFlags & (SOLDIER_DRIVER | SOLDIER_PASSENGER)) + { + ubCanMove = 0; + } + + + INT8 bInWater, bInDeepWater, bInGas; + // determine if we happen to be in water (in which case we're in BIG trouble!) + bInWater = Water(pSoldier->sGridNo, pSoldier->pathing.bLevel); + bInDeepWater = WaterTooDeepForAttacks(pSoldier->sGridNo, pSoldier->pathing.bLevel); + bInGas = FALSE; + pSoldier->aiData.bAIMorale = MORALE_FEARLESS; + + + //////////////////////////////////////////////////////////////////////////// + // STUCK IN WATER OR GAS, NO COVER, GO TO NEAREST SPOT OF UNGASSED LAND + //////////////////////////////////////////////////////////////////////////// + decision = DecideActionStuckInWaterOrGas(pSoldier, ubCanMove, bInWater, bInDeepWater, bInGas); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + + //////////////////////////////////////////////////////////////////////////// + // SOLDIER CAN ATTACK IF NOT IN WATER/GAS AND NOT DOING SOMETHING TOO FUNKY + //////////////////////////////////////////////////////////////////////////// + + // NPCs in water/tear gas without masks are not permitted to shoot/stab/throw + INT8 bCanAttack; + if ((pSoldier->bActionPoints < 2) || bInDeepWater) + { + bCanAttack = FALSE; + } + else + { + do + { + bCanAttack = CanNPCAttack(pSoldier); + if (bCanAttack != TRUE) + { + if (bCanAttack == NOSHOOT_NOAMMO && ubCanMove && !pSoldier->aiData.bNeutral) + { + int handPOS; + //CHRISL: We need to know which weapon has no ammo in case the soldier is holding a weapoin in SECONDHANDPOS + if (pSoldier->inv[SECONDHANDPOS].exists() == true && pSoldier->inv[SECONDHANDPOS][0]->data.gun.ubGunShotsLeft == 0) + handPOS = SECONDHANDPOS; + else + handPOS = HANDPOS; + + // try to find more ammo + pSoldier->aiData.bAction = SearchForItems(pSoldier, SEARCH_AMMO, pSoldier->inv[handPOS].usItem); + + if (pSoldier->aiData.bAction == AI_ACTION_NONE) + { + // the current weapon appears is useless right now! + // (since we got a return code of noammo, we know the hand usItem + // is our gun) + pSoldier->inv[handPOS].fFlags |= OBJECT_AI_UNUSABLE; + // move the gun into another pocket... + if (!AutoPlaceObject(pSoldier, &(pSoldier->inv[handPOS]), FALSE)) + { + // If there's no room in his pockets for the useless gun, just throw it away + return AI_ACTION_DROP_ITEM; + } + } + else + { + return(pSoldier->aiData.bAction); + } + } + else + { + bCanAttack = FALSE; + } + } + } while (bCanAttack != TRUE && bCanAttack != FALSE); + } + + + //////////////////////////////////////////////////////////////////////////// + // RADIO OPERATOR TRAIT + //////////////////////////////////////////////////////////////////////////// + decision = DecideActionRadioOperator(pSoldier, gLogDecideActionBlack); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + + + + //////////////////////////////////////////////////////////////////////////// + // DETERMINE BEST ATTACK + //////////////////////////////////////////////////////////////////////////// + BestShot.ubPossible = FALSE; // by default, assume Shooting isn't possible + BestThrow.ubPossible = FALSE; // by default, assume Throwing isn't possible + BestStab.ubPossible = FALSE; // by default, assume Stabbing isn't possible + BestAttack.ubChanceToReallyHit = 0; + UINT8 ubBestAttackAction = AI_ACTION_NONE; + + // if we are able attack + if (bCanAttack) + { + pSoldier->bAimShotLocation = AIM_SHOT_RANDOM; + + ////////////////////////////////////////////////////////////////////////// + // FIRE A GUN AT AN OPPONENT + ////////////////////////////////////////////////////////////////////////// + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "FIRE A GUN AT AN OPPONENT"); + + CheckIfShotPossible(pSoldier, &BestShot); + + if (BestShot.ubFriendlyFireChance) //dnl ch61 180813 + { + // determine chance to shoot + INT32 iChanceToShoot; + + iChanceToShoot = 100 - BestShot.ubFriendlyFireChance; + iChanceToShoot = iChanceToShoot * iChanceToShoot / 100; + + DebugAI(AI_MSG_INFO, pSoldier, String("Friendly fire chance %d, chance to shoot %d", BestShot.ubFriendlyFireChance, iChanceToShoot)); + + if (Chance(100 - iChanceToShoot)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Friendly fire check failed, skip shooting!")); + BestShot.ubPossible = FALSE; + } + } + + if (BestShot.ubPossible) + { + // if the selected opponent is not a threat (unconscious & !serviced) + // (usually, this means all the guys we see are unconscious, but, on + // rare occasions, we may not be able to shoot a healthy guy, too) + if ((Menptr[BestShot.ubOpponent].stats.bLife < OKLIFE) && + !Menptr[BestShot.ubOpponent].bService && + (pSoldier->aiData.bAttitude != AGGRESSIVE || Chance((100 - BestShot.ubChanceToReallyHit) / 2))) + { + // get the location of the closest CONSCIOUS reachable opponent + BOOLEAN fClimbDummy; + INT32 sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimbDummy); + + // if we found one + if (!TileIsOutOfBounds(sClosestDisturbance)) + { + // then make decision as if at alert status RED + return DecideActionRedRobot(pSoldier); + } + // else kill the guy, he could be the last opponent alive in this sector + } + + // now we KNOW FOR SURE that we will do something (shoot, at least) + NPCDoesAct(pSoldier); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "NPC decided to shoot (or something)"); + } + + ////////////////////////////////////////////////////////////////////////// + // THROW A TOSSABLE ITEM AT OPPONENT(S) + // - HTH: THIS NOW INCLUDES FIRING THE GRENADE LAUNCHAR AND MORTAR! + ////////////////////////////////////////////////////////////////////////// + + // this looks for throwables, and sets BestThrow.ubPossible if it can be done + CheckIfTossPossible(pSoldier, &BestThrow); + + if (BestThrow.ubPossible) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "good throw possible"); + if (ItemIsMortar(pSoldier->inv[BestThrow.bWeaponIn].usItem)) + { + UINT8 ubOpponentDir = AIDirection(pSoldier->sGridNo, BestThrow.sTarget); + + // Get new gridno! + INT32 sCheckGridNo = NewGridNo(pSoldier->sGridNo, (UINT16)DirectionInc(ubOpponentDir)); + + if (!OKFallDirection(pSoldier, sCheckGridNo, pSoldier->pathing.bLevel, ubOpponentDir, pSoldier->usAnimState)) + { + // can't fire! + BestThrow.ubPossible = FALSE; + + // try behind us, see if there's room to move back + sCheckGridNo = NewGridNo(pSoldier->sGridNo, (UINT16)DirectionInc(gOppositeDirection[ubOpponentDir])); + if (OKFallDirection(pSoldier, sCheckGridNo, pSoldier->pathing.bLevel, gOppositeDirection[ubOpponentDir], pSoldier->usAnimState)) + { + // sevenfm: check if we can reach this gridno + INT32 iPathCost = EstimatePlotPath(pSoldier, sCheckGridNo, FALSE, FALSE, FALSE, DetermineMovementMode(pSoldier, AI_ACTION_GET_CLOSER), pSoldier->bStealthMode, FALSE, 0); + if (iPathCost != 0 && iPathCost <= pSoldier->bActionPoints) + { + pSoldier->aiData.usActionData = sCheckGridNo; + return AI_ACTION_GET_CLOSER; + } + } + } + } + + if (BestThrow.ubPossible) + { + // now we KNOW FOR SURE that we will do something (throw, at least) + NPCDoesAct(pSoldier); + } + } + + + ////////////////////////////////////////////////////////////////////////// + // CHOOSE THE BEST TYPE OF ATTACK OUT OF THOSE FOUND TO BE POSSIBLE + ////////////////////////////////////////////////////////////////////////// + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "CHOOSE THE BEST TYPE OF ATTACK OUT OF THOSE FOUND TO BE POSSIBLE"); + BestAttack.iAttackValue = 0; + + if (BestShot.ubPossible) + { + BestAttack.iAttackValue = BestShot.iAttackValue; + ubBestAttackAction = AI_ACTION_FIRE_GUN; + DebugAI(AI_MSG_INFO, pSoldier, String("best action = fire gun, iAttackValue = %d", BestAttack.iAttackValue)); + } + + if (BestThrow.ubPossible && ((BestThrow.iAttackValue > BestAttack.iAttackValue) || (ubBestAttackAction == AI_ACTION_NONE)) && !((ARMED_VEHICLE(pSoldier) || ENEMYROBOT(pSoldier)) && ubBestAttackAction == AI_ACTION_FIRE_GUN && BestShot.ubChanceToReallyHit > 20 && Random(2)))//dnl ch64 290813 tank always had better chance to fire from cannon so this will increase probabilty to use machinegun too + { + ubBestAttackAction = AI_ACTION_TOSS_PROJECTILE; + DebugAI(AI_MSG_INFO, pSoldier, String("best action = throw something, iAttackValue = %d", BestThrow.iAttackValue)); + } + + // copy the information on the best action selected into BestAttack struct + DebugAI(AI_MSG_INFO, pSoldier, String("copy the information on the best action selected into BestAttack struct")); + switch (ubBestAttackAction) + { + case AI_ACTION_FIRE_GUN: + memcpy(&BestAttack, &BestShot, sizeof(BestAttack)); + DebugAI(AI_MSG_INFO, pSoldier, String("Best attack - shooting")); + break; + + case AI_ACTION_TOSS_PROJECTILE: + memcpy(&BestAttack, &BestThrow, sizeof(BestAttack)); + DebugAI(AI_MSG_INFO, pSoldier, String("Best attack - throwing grenade")); + break; + + case AI_ACTION_THROW_KNIFE: + case AI_ACTION_KNIFE_MOVE: + DebugAI(AI_MSG_INFO, pSoldier, String("Best attack - stab")); + memcpy(&BestAttack, &BestStab, sizeof(BestAttack)); + break; + case AI_ACTION_STEAL_MOVE: // added by SANDRO + DebugAI(AI_MSG_INFO, pSoldier, String("Best attack - steal weapon")); + memcpy(&BestAttack, &BestStab, sizeof(BestAttack)); + break; + + default: + // set to empty + DebugAI(AI_MSG_INFO, pSoldier, String("Best attack - no good attack")); + memset(&BestAttack, 0, sizeof(BestAttack)); + break; + } + } + + UINT16 usRange = BestAttack.bWeaponIn == NO_SLOT ? 0 : GetModifiedGunRange(pSoldier->inv[BestAttack.bWeaponIn].usItem);//dnl ch69 150913 + INT32 sClosestThreat = ClosestKnownOpponent(pSoldier, NULL, NULL); + + + //////////////////////////////////////////////////////////////////////////// + // POSSIBLY FORGET THE ATTACK AND TAKE COVER + //////////////////////////////////////////////////////////////////////////// + INT32 sBestCover = NOWHERE; + BOOLEAN fAllowCoverCheck = FALSE; + if ( //(pSoldier->bActionPoints == pSoldier->bInitialActionPoints) && + (ubBestAttackAction == AI_ACTION_FIRE_GUN) && + (pSoldier->aiData.bShock == 0) && + (pSoldier->stats.bLife >= pSoldier->stats.bLifeMax / 2) && + (BestAttack.ubChanceToReallyHit < 30) && + (PythSpacesAway(pSoldier->sGridNo, BestAttack.sTarget) > usRange / CELL_X_SIZE) && + (RangeChangeDesire(pSoldier) >= 4)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Allow taking cover]")); + // okay, really got to wonder about this... could taking cover be an option? + if (ubCanMove && pSoldier->aiData.bOrders != STATIONARY && !gfHiddenInterrupt) + { + // make militia a bit more cautious + // 3 (UINT16) CONVERSIONS HERE TO AVOID ERRORS. GOTTHARD 7/15/08 + if (pSoldier->bTeam == MILITIA_TEAM && (INT16)(PreRandom(20)) > BestAttack.ubChanceToReallyHit || + pSoldier->bTeam != MILITIA_TEAM && (INT16)(PreRandom(40)) > BestAttack.ubChanceToReallyHit) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Allow cover check")); + // maybe taking cover would be better! + fAllowCoverCheck = TRUE; + + sBestCover = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iCoverPercentBetter); + if ((INT16)(PreRandom(10)) > BestAttack.ubChanceToReallyHit && + !TileIsOutOfBounds(sBestCover) && + (iCoverPercentBetter > 10 || !AnyCoverAtSpot(pSoldier, pSoldier->sGridNo))) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "DecideActionBlack: can't hit so screw the attack"); + DebugAI(AI_MSG_INFO, pSoldier, String("can't hit, screw the attack")); + // screw the attack! + ubBestAttackAction = AI_ACTION_NONE; + } + } + } + } + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "LOOK FOR SOME KIND OF COVER BETTER THAN WHAT WE HAVE NOW"); + //////////////////////////////////////////////////////////////////////////// + // LOOK FOR SOME KIND OF COVER BETTER THAN WHAT WE HAVE NOW + //////////////////////////////////////////////////////////////////////////// + + // if soldier has enough APs left to move at least 1 square's worth, + // and either he can't attack any more, or his attack did wound someone + iCoverPercentBetter = 0; + + if ((ubCanMove && !SkipCoverCheck && !gfHiddenInterrupt && + ((ubBestAttackAction == AI_ACTION_NONE) || pSoldier->aiData.bLastAttackHit) && + (pSoldier->bTeam != gbPlayerNum || pSoldier->aiData.fAIFlags & AI_RTP_OPTION_CAN_SEEK_COVER)) + || fAllowCoverCheck) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Find cover]")); + // sevenfm: if not found yet + if (TileIsOutOfBounds(sBestCover)) + { + sBestCover = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iCoverPercentBetter); + } + DebugAI(AI_MSG_INFO, pSoldier, String("Found cover spot %d percent better %d movement mode %d", sBestCover, iCoverPercentBetter, DetermineMovementMode(pSoldier, AI_ACTION_TAKE_COVER))); + } + + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "DecideActionBlack: DECIDE BETWEEN ATTACKING AND DEFENDING (TAKING COVER)"); + ////////////////////////////////////////////////////////////////////////// + // IF NECESSARY, DECIDE BETWEEN ATTACKING AND DEFENDING (TAKING COVER) + ////////////////////////////////////////////////////////////////////////// + + // if both are possible + if ((ubBestAttackAction != AI_ACTION_NONE) && (!TileIsOutOfBounds(sBestCover))) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide attack/cover]")); + // gotta compare their merits and select the more desirable option + iOffense = BestAttack.ubChanceToReallyHit; + iDefense = iCoverPercentBetter; + + // based on how we feel about the situation, decide whether to attack first + switch (pSoldier->aiData.bAIMorale) + { + case MORALE_FEARLESS: + iOffense += iOffense / 2; // increase 50% + break; + + case MORALE_CONFIDENT: + iOffense += iOffense / 4; // increase 25% + break; + + case MORALE_NORMAL: + break; + + case MORALE_WORRIED: + iDefense += iDefense / 4; // increase 25% + break; + + case MORALE_HOPELESS: + iDefense += iDefense / 2; // increase 50% + break; + } + + + // smart guys more likely to try to stay alive, dolts more likely to shoot! + if (pSoldier->stats.bWisdom >= 50) //Madd: reduced the wisdom required to want to live... + iDefense += 10; + else if (pSoldier->stats.bWisdom < 30) + iDefense -= 10; + + // some orders are more offensive, others more defensive + if (pSoldier->aiData.bOrders == SEEKENEMY) + iOffense += 10; + else if ((pSoldier->aiData.bOrders == STATIONARY) || (pSoldier->aiData.bOrders == ONGUARD) || pSoldier->aiData.bOrders == SNIPER) + iDefense += 10; + + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iDefense += 30; break; + case BRAVESOLO: iDefense -= 0; break; + case BRAVEAID: iDefense -= 0; break; + case CUNNINGSOLO: iDefense += 20; break; + case CUNNINGAID: iDefense += 20; break; + case AGGRESSIVE: iOffense += 10; break; + case ATTACKSLAYONLY:iOffense += 30; break; + } + + DebugAI(AI_MSG_INFO, pSoldier, String("iOffense %d iDefense %d", iOffense, iDefense)); + + // if his defensive instincts win out, forget all about the attack + if (iDefense > iOffense) + { + DebugAI(AI_MSG_INFO, pSoldier, String("[decided taking cover, disable attack]")); + ubBestAttackAction = AI_ACTION_NONE; + } + } + + + ////////////////////////////////////////////////////////////////////////// + // PREPARE ATTACK + ////////////////////////////////////////////////////////////////////////// + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionBlack: is attack still desirable? ubBestAttackAction = %d", ubBestAttackAction)); + + // if attack is still desirable (meaning it's also preferred to taking cover) + if (ubBestAttackAction != AI_ACTION_NONE) + { + //DebugAI(AI_MSG_TOPIC, pSoldier, String("[Attack]")); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Prepare attack]")); + // if we wanted to be REALLY mean, we could look at chance to hit and decide whether + // to shoot at the head... + + // default settings + //POSSIBLE STRUCTURE CHANGE PROBLEM, NOT CURRENTLY CHANGED. GOTTHARD 7/14/08 + pSoldier->aiData.bAimTime = BestAttack.ubAimTime; + pSoldier->bScopeMode = BestAttack.bScopeMode; + pSoldier->bDoBurst = 0; + + // HEADROCK HAM 3.6: bAimTime represents how MANY aiming levels are used, not how much APs they cost necessarily. + INT16 sActualAimAP = CalcAPCostForAiming(pSoldier, BestAttack.sTarget, (INT8)pSoldier->aiData.bAimTime); + + INT16 ubBurstAPs; + if (ubBestAttackAction == AI_ACTION_FIRE_GUN) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Prepare shooting]")); + + ////////////////////////////////////////////////////////////////////////// + // IF ENOUGH APs TO BURST, RANDOM CHANCE OF DOING SO + ////////////////////////////////////////////////////////////////////////// + + if (IsGunBurstCapable(&pSoldier->inv[BestAttack.bWeaponIn], FALSE, pSoldier) && + !(Menptr[BestShot.ubOpponent].stats.bLife < OKLIFE) && // don't burst at downed targets + pSoldier->inv[BestAttack.bWeaponIn][0]->data.gun.ubGunShotsLeft > 1 && + pSoldier->bTeam != gbPlayerNum) + { + DebugAI(AI_MSG_INFO, pSoldier, String("enough APs to burst, random chance of doing so")); + + ubBurstAPs = CalcAPsToBurst(pSoldier->CalcActionPoints(), &(pSoldier->inv[BestAttack.bWeaponIn]), pSoldier); + + // HEADROCK HAM 3.6: Use Actual Aiming Time. + if (pSoldier->bActionPoints >= BestAttack.ubAPCost + sActualAimAP + ubBurstAPs) + { + if (ARMED_VEHICLE(pSoldier) || ENEMYROBOT(pSoldier)) + { + iChance = 100; + } + + if ((INT32)PreRandom(100) < iChance) + { + BestAttack.ubAPCost += ubBurstAPs + sActualAimAP;//dnl ch58 130913 + // check for spread burst possibilities + if (pSoldier->aiData.bAttitude != ATTACKSLAYONLY) + { + CalcSpreadBurst(pSoldier, BestAttack.sTarget, BestAttack.bTargetLevel); + } + //dnl ch58 130913 return aiming for burst + pSoldier->bDoBurst = 1; + pSoldier->bDoAutofire = 0; + } + } + } + + if (IsGunAutofireCapable(&pSoldier->inv[BestAttack.bWeaponIn]) && + !(Menptr[BestShot.ubOpponent].stats.bLife < OKLIFE) && // don't burst at downed targets + ((pSoldier->inv[BestAttack.bWeaponIn][0]->data.gun.ubGunShotsLeft > 1 && + !pSoldier->bDoBurst) || Weapon[pSoldier->inv[BestAttack.bWeaponIn].usItem].NoSemiAuto)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("enough APs to autofire, random chance of doing so")); + L_NEWAIM: + FLOAT dTotalRecoil = 0.0f; + pSoldier->bDoAutofire = 0; + if (UsingNewCTHSystem() == true) + { + do + { + pSoldier->bDoAutofire++; + dTotalRecoil += AICalcRecoilForShot(pSoldier, &(pSoldier->inv[BestShot.bWeaponIn]), pSoldier->bDoAutofire); + ubBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &(pSoldier->inv[BestShot.bWeaponIn]), pSoldier->bDoAutofire, pSoldier); + } while (pSoldier->bActionPoints >= BestShot.ubAPCost + ubBurstAPs + sActualAimAP && pSoldier->inv[BestAttack.bWeaponIn][0]->data.gun.ubGunShotsLeft >= pSoldier->bDoAutofire && dTotalRecoil <= 10.0f);//dnl ch64 260813 pSoldier->ubAttackingHand is wrong because decision is to use BestAttack.bWeaponIn + } + else + { + do + { + pSoldier->bDoAutofire++; + ubBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &(pSoldier->inv[BestAttack.bWeaponIn]), pSoldier->bDoAutofire, pSoldier); + } while (pSoldier->bActionPoints >= BestAttack.ubAPCost + ubBurstAPs + sActualAimAP && pSoldier->inv[BestAttack.bWeaponIn][0]->data.gun.ubGunShotsLeft >= pSoldier->bDoAutofire && GetAutoPenalty(&pSoldier->inv[BestAttack.bWeaponIn], gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_PRONE) * pSoldier->bDoAutofire <= 80);//dnl ch64 130913 pSoldier->ubAttackingHand is wrong because decision is to use BestAttack.bWeaponIn, also missing sActualAimTime + } + + pSoldier->bDoAutofire--; + + DebugAI(AI_MSG_INFO, pSoldier, String("autofire %d", pSoldier->bDoAutofire)); + + //dnl ch69 130913 let try increase autofire rate for aim cost + // sevenfm: LIMIT_MAX_DEVIATION option increases effectiveness of suppression + if ((!UsingNewCTHSystem() || gGameCTHConstants.LIMIT_MAX_DEVIATION) && + pSoldier->bDoAutofire < 3 && + pSoldier->aiData.bAimTime > 0 && + pSoldier->inv[BestAttack.bWeaponIn][0]->data.gun.ubGunShotsLeft >= 3 && + Chance(gGameExternalOptions.sSuppressionEffectiveness) && + (!gGameExternalOptions.fAISafeSuppression || CheckSuppressionDirection(pSoldier, BestShot.sTarget, BestShot.bTargetLevel))) + { + pSoldier->aiData.bAimTime--; + if (pSoldier->aiData.bAimTime < 0) { pSoldier->aiData.bAimTime = 0; } + + sActualAimAP = CalcAPCostForAiming(pSoldier, BestAttack.sTarget, (INT8)pSoldier->aiData.bAimTime); + DebugAI(AI_MSG_INFO, pSoldier, String("reduce aim to %d, recalc autofire, aim AP %d", pSoldier->aiData.bAimTime, sActualAimAP)); + goto L_NEWAIM; + } + + if (pSoldier->bDoAutofire > 0) + { + ubBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &(pSoldier->inv[BestAttack.bWeaponIn]), pSoldier->bDoAutofire, pSoldier); + + if (pSoldier->bActionPoints >= BestAttack.ubAPCost + sActualAimAP + ubBurstAPs) + { + // Base chance of bursting is 25% if best shot was +0 aim, down to 8% at +4 + if (ARMED_VEHICLE(pSoldier) || ENEMYROBOT(pSoldier)) + { + iChance = 100; + } + + DebugAI(AI_MSG_INFO, pSoldier, String("chance for autofire %d", iChance)); + + if ((INT32)PreRandom(100) < iChance || Weapon[pSoldier->inv[BestAttack.bWeaponIn].usItem].NoSemiAuto) + { + //dnl ch69 140913 return aiming for autofire with halfautofire fix + pSoldier->bDoBurst = 1; + INT16 ubHalfBurstAPs = 256; + if (pSoldier->inv[BestAttack.bWeaponIn][0]->data.gun.ubGunShotsLeft < 4) + { + iChance = 0; + } + else + { + ubHalfBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &pSoldier->inv[BestAttack.bWeaponIn], 2, pSoldier); + + if (!CheckSuppressionDirection(pSoldier, BestAttack.sTarget, BestAttack.bTargetLevel)) + iChance = 100; + else + iChance = BestAttack.ubChanceToReallyHit / 2; + + if (Weapon[pSoldier->inv[BestAttack.bWeaponIn].usItem].NoSemiAuto || pSoldier->aiData.bOppCnt > 1) + iChance += (100 - iChance) / 2; + } + + if (Chance(iChance) && pSoldier->bActionPoints >= (2 * BestAttack.ubAPCost + ubHalfBurstAPs + sActualAimAP)) + { + // Try short autofire to enhance chance of hitting + pSoldier->bDoAutofire = 2; + BestAttack.ubAPCost += ubHalfBurstAPs + sActualAimAP; + } + else + { + BestAttack.ubAPCost += ubBurstAPs + sActualAimAP; + } + } + else + { + pSoldier->bDoAutofire = 0; + pSoldier->bDoBurst = 0; + } + } + } + } + + if (!pSoldier->bDoBurst) + { + pSoldier->aiData.bAimTime = BestAttack.ubAimTime; + pSoldier->bDoBurst = 0; + pSoldier->bDoAutofire = 0; + } + + // IF WAY OUT OF EFFECTIVE RANGE TRY TO ADVANCE RESERVING ENOUGH AP FOR A SHOT IF NOT ACTED YET + if ((pSoldier->bActionPoints > BestAttack.ubAPCost) && + (pSoldier->aiData.bShock == 0) && + (pSoldier->stats.bLife >= pSoldier->stats.bLifeMax / 2) && + (BestAttack.ubChanceToReallyHit < 8) && + (PythSpacesAway(pSoldier->sGridNo, BestAttack.sTarget) > usRange / CELL_X_SIZE) && + (RangeChangeDesire(pSoldier) >= 3)) // Cunning and above + { + sClosestOpponent = Menptr[BestShot.ubOpponent].sGridNo; + + DebugAI(AI_MSG_INFO, pSoldier, String("check if can advance to closest opponent %d", sClosestOpponent)); + + if (!TileIsOutOfBounds(sClosestOpponent)) + { + // temporarily make merc get closer reserving enough for expected cost of shot + USHORT tgrd = pSoldier->aiData.sPatrolGrid[0]; + INT8 oldOrders = pSoldier->aiData.bOrders; + pSoldier->aiData.sPatrolGrid[0] = pSoldier->sGridNo; + pSoldier->aiData.bOrders = CLOSEPATROL; + // Try to find a cover spot near opponent + iCoverPercentBetter = 0; + INT32 spotNearTarget = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iCoverPercentBetter, sClosestOpponent); + if (spotNearTarget != NOWHERE) + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, spotNearTarget, BestAttack.ubAPCost, AI_ACTION_GET_CLOSER, 0); + + } + else + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestOpponent, BestAttack.ubAPCost, AI_ACTION_GET_CLOSER, 0); + } + pSoldier->aiData.sPatrolGrid[0] = tgrd; + pSoldier->aiData.bOrders = oldOrders; + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + pSoldier->aiData.bNextAction = AI_ACTION_FIRE_GUN; + pSoldier->aiData.usNextActionData = BestAttack.sTarget; + pSoldier->aiData.bNextTargetLevel = BestAttack.bTargetLevel; + + DebugAI(AI_MSG_INFO, pSoldier, String("try to get closer before shooting, move to %d", pSoldier->aiData.usActionData)); + return(AI_ACTION_GET_CLOSER); + } + } + } + } + + ////////////////////////////////////////////////////////////////////////// + // OTHERWISE, JUST GO AHEAD & ATTACK! + ////////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_TOPIC, pSoldier, String("Attack!")); + + //dnl ch64 270813 must be as below RearrangePocket with FOREVER will screw already decided BURST or AUTOFIRE + INT8 bDoBurst = pSoldier->bDoBurst; + UINT8 bDoAutofire = pSoldier->bDoAutofire; + // swap weapon to hand if necessary + if (BestAttack.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("swap weapon into hand from %d", BestAttack.bWeaponIn)); + RearrangePocket(pSoldier, HANDPOS, BestAttack.bWeaponIn, FOREVER); + } + + if (ubBestAttackAction == AI_ACTION_FIRE_GUN && bDoBurst == 1)//dnl ch64 270813 + { + DebugAI(AI_MSG_INFO, pSoldier, String("using burst/autofire")); + + pSoldier->bDoAutofire = bDoAutofire; + pSoldier->bDoBurst = bDoBurst; + if (bDoAutofire > 1) + pSoldier->bWeaponMode = WM_AUTOFIRE; + else + pSoldier->bWeaponMode = WM_BURST; + } + + DebugAI(AI_MSG_INFO, pSoldier, String("prepare attack at target %d level %d aim %d ap %d cth %d opponent %d", BestAttack.sTarget, BestAttack.bTargetLevel, BestAttack.ubAimTime, BestAttack.ubAPCost, BestAttack.ubChanceToReallyHit, BestAttack.ubOpponent)); + + if (ubBestAttackAction == AI_ACTION_FIRE_GUN) + { + if (gAnimControl[pSoldier->usAnimState].ubEndHeight != BestAttack.ubStance && + IsValidStance(pSoldier, BestAttack.ubStance)) + { + pSoldier->aiData.bNextAction = AI_ACTION_FIRE_GUN; + pSoldier->aiData.usNextActionData = BestAttack.sTarget; + pSoldier->aiData.bNextTargetLevel = BestAttack.bTargetLevel; + pSoldier->aiData.usActionData = BestAttack.ubStance; + + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before shooting")); + return(AI_ACTION_CHANGE_STANCE); + } + else + { + pSoldier->aiData.usActionData = BestAttack.sTarget; + pSoldier->bTargetLevel = BestAttack.bTargetLevel; + DebugAI(AI_MSG_INFO, pSoldier, String("Fire weapon!")); + return(AI_ACTION_FIRE_GUN); + } + } + else if (ubBestAttackAction == AI_ACTION_TOSS_PROJECTILE) + { + DebugAI(AI_MSG_INFO, pSoldier, String("toss attack, disable burst/autofire")); + pSoldier->bDoBurst = 0; + pSoldier->bDoAutofire = 0; + + if (IsGrenadeLauncherAttached(&pSoldier->inv[HANDPOS])) //dnl ch63 240813 + { + DebugAI(AI_MSG_INFO, pSoldier, String("using attached GL")); + pSoldier->bWeaponMode = WM_ATTACHED_GL; + } + + // stand up before throwing if needed + if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestAttack.ubStance && + pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestAttack.sTarget), BestAttack.ubStance)) + { + pSoldier->aiData.usActionData = BestAttack.ubStance; + pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; + pSoldier->aiData.usNextActionData = BestAttack.sTarget; + pSoldier->aiData.bNextTargetLevel = BestAttack.bTargetLevel; + return AI_ACTION_CHANGE_STANCE; + } + else + { + pSoldier->aiData.usActionData = BestAttack.sTarget; + pSoldier->bTargetLevel = BestAttack.bTargetLevel; + return(AI_ACTION_TOSS_PROJECTILE); + } + } + // other attacks + else + { + pSoldier->aiData.usActionData = BestAttack.sTarget; + pSoldier->bTargetLevel = BestAttack.bTargetLevel; + return(ubBestAttackAction); + } + } + + + //////////////////////////////////////////////////////////////////////////// + // IF A LOCATION WITH BETTER COVER IS AVAILABLE & REACHABLE, GO FOR IT! + //////////////////////////////////////////////////////////////////////////// + if (!TileIsOutOfBounds(sBestCover)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Take cover]")); +#ifdef DEBUGDECISIONS + STR tempstr = ""; + sprintf(tempstr, "%s - TAKING COVER at gridno %d (%d%% better)\n", + pSoldier->name, sBestCover, iCoverPercentBetter); + DebugAI(tempstr); +#endif + //ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d taking cover, morale %d, from %d to %d", pSoldier->ubID, pSoldier->aiData.bAIMorale, pSoldier->sGridNo, sBestCover ); + pSoldier->aiData.usActionData = sBestCover; + if (!TileIsOutOfBounds(sClosestOpponent))//dnl ch58 150913 After taking cover change facing toward recent target or closest enemy, currently such turn not charge APs and seems because AI is still in moving animation from take cover action + { + if (!TileIsOutOfBounds(pSoldier->sLastTarget)) + sClosestOpponent = pSoldier->sLastTarget; + pSoldier->aiData.bNextAction = AI_ACTION_CHANGE_FACING; + pSoldier->aiData.usNextActionData = GetDirectionFromCenterCellXYGridNo(sBestCover, sClosestOpponent); + } + return(AI_ACTION_TAKE_COVER); + } + + + + //////////////////////////////////////////////////////////////////////////// + // IF SPOTTERS HAVE BEEN CALLED FOR, AND WE HAVE SOME NEW SIGHTINGS, RADIO! + //////////////////////////////////////////////////////////////////////////// + + // if we're a computer merc, and we have the action points remaining to RADIO + // (we never want NPCs to choose to radio if they would have to wait a turn) + // and we're not swimming in deep water, and somebody has called for spotters + // and we see the location of at least 2 opponents + if (!(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT) && (gTacticalStatus.ubSpottersCalledForBy != NOBODY) && (pSoldier->bActionPoints >= APBPConstants[AP_RADIO]) && + (pSoldier->aiData.bOppCnt > 1) && + (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1) && !bInDeepWater) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Radio sightings]")); + // base chance depends on how much new info we have to radio to the others + iChance = 25 * WhatIKnowThatPublicDont(pSoldier, TRUE); // just count them + + // if I actually know something they don't + if (iChance) + { + if ((INT16)PreRandom(100) < iChance) + { + return(AI_ACTION_RED_ALERT); + } + } + } + + + + //////////////////////////////////////////////////////////////////////////// + // TURN TO FACE CLOSEST KNOWN OPPONENT (IF NOT FACING THERE ALREADY) + //////////////////////////////////////////////////////////////////////////// + decision = DecideActionChangeFacing(pSoldier, ubCanMove, gLogDecideActionBlack); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + + + //////////////////////////////////////////////////////////////////////////// + // DO NOTHING: Not enough points left to move, so save them for next turn + //////////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Nothing to do]")); + // by default, if everything else fails, just stand in place and wait + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); +} + + + +//////////////////////////////////////////////////////////////////////////// +// Soldier AI +//////////////////////////////////////////////////////////////////////////// +INT8 DecideActionGreenSoldier(SOLDIERTYPE* pSoldier) +{ + DebugAI(AI_MSG_START, pSoldier, String("[Green Soldier]")); + LogDecideInfo(pSoldier); + + auto decision = AI_ACTION_INVALID; + + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // sevenfm: initialize data + pSoldier->bWeaponMode = WM_NORMAL; + + gubNPCPathCount = 0; + + INT8 bInWater = Water(pSoldier->sGridNo, pSoldier->pathing.bLevel); + INT8 bInDeepWater = DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel); + INT8 bInGas = InGasOrSmoke(pSoldier, pSoldier->sGridNo); + + // if real-time, and not in the way, do nothing 90% of the time (for GUARDS!) + // unless in water (could've started there), then we better swim to shore! + if (gGameExternalOptions.fAllNamedNpcsDecideAction && pSoldier->ubProfile != NO_PROFILE) + { + if (pSoldier->flags.uiStatusFlags & SOLDIER_COWERING) + { + // everything's peaceful again, stop cowering!! + pSoldier->aiData.usActionData = ANIM_STAND; + return(AI_ACTION_STOP_COWERING); + } + + if (!gfTurnBasedAI) + { + // ****************** + // REAL TIME NPC CODE + // ****************** + if (pSoldier->aiData.fAIFlags & AI_CHECK_SCHEDULE) + { + pSoldier->aiData.bAction = DecideActionSchedule(pSoldier); + if (pSoldier->aiData.bAction != AI_ACTION_NONE) + { + return(pSoldier->aiData.bAction); + } + } + + if (pSoldier->ubProfile != NO_PROFILE || pSoldier->IsAssassin()) + { + if (pSoldier->ubProfile != NO_PROFILE) + pSoldier->aiData.bAction = DecideActionNamedNPC(pSoldier); + else + { + INT32 sDesiredMercDist; + INT32 sDesiredMercLoc = ClosestUnDisguisedPC(pSoldier, &sDesiredMercDist); + + if (!TileIsOutOfBounds(sDesiredMercLoc)) + { + if (sDesiredMercDist <= NPC_TALK_RADIUS * 2) + { + AddToShouldBecomeHostileOrSayQuoteList(pSoldier->ubID); + // now wait a bit! + pSoldier->aiData.usActionData = 5000; + pSoldier->aiData.bAction = AI_ACTION_WAIT; + } + else + { + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sDesiredMercLoc, AI_ACTION_APPROACH_MERC); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + pSoldier->aiData.bAction = AI_ACTION_APPROACH_MERC; + } + } + } + } + + if (pSoldier->aiData.bAction != AI_ACTION_NONE) + { + return(pSoldier->aiData.bAction); + } + // can we act again? not for a minute since we were last spoken to/triggered a record + if (pSoldier->uiTimeSinceLastSpoke && (GetJA2Clock() < pSoldier->uiTimeSinceLastSpoke + 60000)) + { + return(AI_ACTION_NONE); + } + // turn off counter so we don't check it again + pSoldier->uiTimeSinceLastSpoke = 0; + } + } + + // if not in the way, do nothing most of the time + // unless in water (could've started there), then we better swim to shore! + + if (!(bInDeepWater) && PreRandom(5)) + { + // don't do nuttin! + return(AI_ACTION_NONE); + } + } + + //ddd{ + if (!(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT) && gGameExternalOptions.bNewTacticalAIBehavior && pSoldier->bTeam == ENEMY_TEAM) + { + if (!(gTacticalStatus.uiFlags & TURNBASED) && (gTacticalStatus.uiFlags & INCOMBAT)) + { + INT32 cnt; + ROTTING_CORPSE* pCorpse; + + for (cnt = 0; cnt < giNumRottingCorpse; ++cnt) + { + pCorpse = &(gRottingCorpse[cnt]); + + if (pCorpse->fActivated && pCorpse->def.ubAIWarningValue > 0) + { + if (PythSpacesAway(pSoldier->sGridNo, pCorpse->def.sGridNo) <= 5)//add check(comparison) of sight range variable (smaxvid ?) + { + //check if the corpse is in the enemy/militia field of view? + if (SoldierTo3DLocationLineOfSightTest(pSoldier, pCorpse->def.sGridNo, pCorpse->def.bLevel, 3, TRUE, CALC_FROM_WANTED_DIR)) + { + ScreenMsg(MSG_FONT_YELLOW, MSG_INTERFACE, New113Message[MSG113_ENEMY_FOUND_DEAD_BODY]); + //pCorpse->def.ubAIWarningValue=0; + gRottingCorpse[cnt].def.ubAIWarningValue = 0; + return(AI_ACTION_RED_ALERT); + } + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // IF YOU SEE CAPTURED FRIENDS, FREE THEM! + //////////////////////////////////////////////////////////////////////////// + + // Flugente: if we see one of our buddies in handcuffs, its a clear sign of enemy activity! + if (gGameExternalOptions.fAllowPrisonerSystem && pSoldier->bTeam == ENEMY_TEAM && !gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition) + { + SoldierID ubPerson = GetClosestFlaggedSoldierID(pSoldier, 20, ENEMY_TEAM, SOLDIER_POW, TRUE); + + if (ubPerson != NOBODY) + { + // raise alarm! + return(AI_ACTION_RED_ALERT); + } + } + + // if we are a doctor with medical gear, we might be able to help a wounded ally + if (pSoldier->CanMedicAI()) + { + SoldierID ubPerson = GetClosestWoundedSoldierID(pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius, pSoldier->bTeam); + + // are we ourselves the patient? + if (ubPerson == pSoldier->ubID) + { + // if not already crouched, crouch down first + if (gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_CROUCH && IsValidStance(pSoldier, ANIM_CROUCH) && GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints) + { + pSoldier->aiData.usActionData = ANIM_CROUCH; + + return(AI_ACTION_CHANGE_STANCE); + } + + return(AI_ACTION_DOCTOR_SELF); + } + else if (ubPerson != NOBODY) + { + if (PythSpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) < 2) + { + // see if we are facing this person + UINT8 ubDesiredMercDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, ubPerson->sGridNo); + + // if not already facing in that direction, + if (pSoldier->ubDirection != ubDesiredMercDir) + { + pSoldier->aiData.usActionData = ubDesiredMercDir; + + return(AI_ACTION_CHANGE_FACING); + } + + // if not already crouched, crouch down first + if (gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_CROUCH && IsValidStance(pSoldier, ANIM_CROUCH) && GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints) + { + pSoldier->aiData.usActionData = ANIM_CROUCH; + + return(AI_ACTION_CHANGE_STANCE); + } + + return(AI_ACTION_DOCTOR); + } + else + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + // if we are not a medic, but are wounded, seek a medic + else if (pSoldier->iHealableInjury >= gGameExternalOptions.sEnemyMedicsWoundMinAmount) + { + SoldierID ubPerson = GetClosestMedicSoldierID(pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius / 2, pSoldier->bTeam); + + if (ubPerson != NOBODY) + { + if (PythSpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) > 1) + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + + // are we a bodyguard? + if (pSoldier->usSoldierFlagMask & SOLDIER_BODYGUARD) + { + // is VIP still alive? + SoldierID ubPerson = GetClosestFlaggedSoldierID(pSoldier, 100, pSoldier->bTeam, SOLDIER_VIP, FALSE); + + if (ubPerson != NOBODY) + { + // we want to stay close to him, but still be able to function properly... stay withing a 7-tile radius + if (SpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) > 7) + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + } + //ddd} + + //////////////////////////////////////////////////////////////////////////// + // POINT PATROL: move towards next point unless getting a bit winded + //////////////////////////////////////////////////////////////////////////// + + // this takes priority over water/gas checks, so that point patrol WILL work + // from island to island, and through gas covered areas, too + if ((pSoldier->aiData.bOrders == POINTPATROL) && (pSoldier->bBreath >= 75)) + { + if (PointPatrolAI(pSoldier)) + { + if (!gfTurnBasedAI) + { + // wait after this... + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = RealtimeDelay(pSoldier); + } + return(AI_ACTION_POINT_PATROL); + } + else + { + // Reset path count to avoid dedlok + gubNPCPathCount = 0; + } + } + + if ((pSoldier->aiData.bOrders == RNDPTPATROL) && (pSoldier->bBreath >= 75)) + { + if (RandomPointPatrolAI(pSoldier)) + { + if (!gfTurnBasedAI) + { + // wait after this... + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = RealtimeDelay(pSoldier); + } + return(AI_ACTION_POINT_PATROL); + } + else + { + // Reset path count to avoid dedlok + gubNPCPathCount = 0; + } + + } + + //////////////////////////////////////////////////////////////////////////// + // WHEN LEFT IN WATER OR GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND + //////////////////////////////////////////////////////////////////////////// + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: get out of water and gas")); + + if (bInDeepWater || bInGas || FindBombNearby(pSoldier, pSoldier->sGridNo, BOMB_DETECTION_RANGE) || RedSmokeDanger(pSoldier->sGridNo, pSoldier->pathing.bLevel)) + { + pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - SEEKING NEAREST UNGASSED LAND at grid %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + return(AI_ACTION_LEAVE_WATER_GAS); + } + } + + + + //////////////////////////////////////////////////////////////////////// + // REST IF RUNNING OUT OF BREATH + //////////////////////////////////////////////////////////////////////// + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: rest if running out of breath")); + // if our breath is running a bit low, and we're not in the way or in water + if ((pSoldier->bBreath < 75) && !bInWater) + { + // take a breather for gods sake! + // for realtime, AI will use a standard wait set outside of here + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); + } + + + //////////////////////////////////////////////////////////////////////////// + // CLIMB A BUILDING + //////////////////////////////////////////////////////////////////////////// + + if (pSoldier->CheckInitialAP() && + pSoldier->aiData.bLastAction != AI_ACTION_CLIMB_ROOF && + pSoldier->aiData.bOrders != STATIONARY && + pSoldier->pathing.bLevel == 0 && + !is_networked) + { + INT32 iChance = 10 + pSoldier->aiData.bBypassToGreen; + + // set base chance and maximum seeking distance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance *= 0; break; + case ONGUARD: iChance += 10; break; + case ONCALL: break; + case CLOSEPATROL: iChance += -20; break; + case RNDPTPATROL: + case POINTPATROL: iChance = -30; break; + case FARPATROL: iChance += -40; break; + case SEEKENEMY: iChance += -30; break; + case SNIPER: iChance += 70; break; + } + + // modify for attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance *= 1.5; break; + case BRAVESOLO: iChance /= 2; break; + case BRAVEAID: iChance /= 2; break; + case CUNNINGSOLO: iChance *= 1; break; + case CUNNINGAID: iChance /= 1; break; + case AGGRESSIVE: iChance /= 3; break; + case ATTACKSLAYONLY: break; + } + + + //hide those suicidal militia on the roofs for better defensive positions + // 0verhaul: If they are allowed at all to move + if (pSoldier->bTeam == MILITIA_TEAM && iChance != 0) + iChance += 20; + + // reduce chance for any injury, less likely to hop up if hurt + iChance -= (pSoldier->stats.bLifeMax - pSoldier->stats.bLife); + + // reduce chance if breath is down + //iChance -= (100 - pSoldier->bBreath); // don't care + + // This is the chance that we want to be on the roof. If already there, invert the chance to see if we want back + // down + if (pSoldier->pathing.bLevel > 0) + { + iChance = 100 - iChance; + } + + if ((INT16)PreRandom(100) < iChance) + { + BOOLEAN fUp = FALSE; + if (pSoldier->pathing.bLevel == 0) + { + fUp = TRUE; + } + else if (pSoldier->pathing.bLevel > 0) + { + fUp = FALSE; + } + + if (CanClimbFromHere(pSoldier, fUp)) + { + DebugMsg(TOPIC_JA2AI, DBG_LEVEL_3, String("Soldier %d is climbing roof", pSoldier->ubID)); + return(AI_ACTION_CLIMB_ROOF); + } + else + { + pSoldier->aiData.usActionData = FindClosestClimbPoint(pSoldier, fUp); + // Added the check here because sniper militia who are locked inside of a building without keys + // will still have a >100% chance to want to climb, which means an infinite loop. In fact, any + // time a move is desired, there probably also will be a need to check for a path. + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData) && + LegalNPCDestination(pSoldier, pSoldier->aiData.usActionData, ENSURE_PATH, WATEROK, 0)) + { + return(AI_ACTION_MOVE_TO_CLIMB); + } + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // RANDOM PATROL: determine % chance to start a new patrol route + //////////////////////////////////////////////////////////////////////////// + if (!gubNPCPathCount) // try to limit pathing in Green AI + { + INT32 iSneaky = 10; + INT32 iChance = 25 + pSoldier->aiData.bBypassToGreen; + + // set base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += -20; break; + case ONGUARD: iChance += -15; break; + case ONCALL: break; + case CLOSEPATROL: iChance += +15; break; + case RNDPTPATROL: + case POINTPATROL: iChance = 0; break; + case FARPATROL: iChance += +25; break; + case SEEKENEMY: iChance += -10; break; + case SNIPER: iChance += -10; break; + } + + // modify chance of patrol (and whether it's a sneaky one) by attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += -10; break; + case BRAVESOLO: iChance += 5; break; + case BRAVEAID: break; + case CUNNINGSOLO: iChance += 5; iSneaky += 10; break; + case CUNNINGAID: iSneaky += 5; break; + case AGGRESSIVE: iChance += 10; iSneaky += -5; break; + case ATTACKSLAYONLY: iChance += 10; iSneaky += -5; break; + } + + // reduce chance for any injury, less likely to wander around when hurt + iChance -= (pSoldier->stats.bLifeMax - pSoldier->stats.bLife); + + // reduce chance if breath is down, less likely to wander around when tired + iChance -= (100 - pSoldier->bBreath); + + + // if we're in water with land miles (> 25 tiles) away, + // OR if we roll under the chance calculated + if (bInWater || ((INT16)PreRandom(100) < iChance)) + { + pSoldier->aiData.usActionData = RandDestWithinRange(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, pSoldier->aiData.usActionData, AI_ACTION_RANDOM_PATROL); + } + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + if (!gfTurnBasedAI) + { + // wait after this... + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = RealtimeDelay(pSoldier); + } + return(AI_ACTION_RANDOM_PATROL); + } + } + } + + if (!gubNPCPathCount) // try to limit pathing in Green AI + { + //////////////////////////////////////////////////////////////////////////// + // SEEK FRIEND: determine %chance for man to pay a friendly visit + //////////////////////////////////////////////////////////////////////////// + + INT32 iChance = 25 + pSoldier->aiData.bBypassToGreen; + + // set base chance and maximum seeking distance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += -20; break; + case ONGUARD: iChance += -15; break; + case ONCALL: break; + case CLOSEPATROL: iChance += +10; break; + case RNDPTPATROL: + case POINTPATROL: iChance = -10; break; + case FARPATROL: iChance += +20; break; + case SEEKENEMY: iChance += -10; break; + case SNIPER: iChance += -10; break; + } + + // modify for attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: break; + case BRAVESOLO: iChance /= 2; break; // loners + case BRAVEAID: iChance += 10; break; // friendly + case CUNNINGSOLO: iChance /= 2; break; // loners + case CUNNINGAID: iChance += 10; break; // friendly + case AGGRESSIVE: break; + case ATTACKSLAYONLY: break; + } + + // reduce chance for any injury, less likely to wander around when hurt + iChance -= (pSoldier->stats.bLifeMax - pSoldier->stats.bLife); + + // reduce chance if breath is down + iChance -= (100 - pSoldier->bBreath); // very likely to wait when exhausted + + + if ((INT16)PreRandom(100) < iChance) + { + if (RandomFriendWithin(pSoldier)) + { + if (pSoldier->aiData.usActionData == GoAsFarAsPossibleTowards(pSoldier, pSoldier->aiData.usActionData, AI_ACTION_SEEK_FRIEND)) + { + if (!gfTurnBasedAI) + { + // pause at the end of the walk! + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = (UINT16)REALTIME_CIV_AI_DELAY; + } + + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // SNIPERS LIKE TO CROUCH (on roofs) + //////////////////////////////////////////////////////////////////////////// + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Snipers like to crouch, sniper = %d", pSoldier->sniper)); + // if not in water and not already crouched, try to crouch down first + if (pSoldier->aiData.bOrders == SNIPER && !PTR_CROUCHED && IsValidStance(pSoldier, ANIM_CROUCH) && pSoldier->pathing.bLevel == 1) + { + if (!gfTurnBasedAI || (GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints)) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Sniper is crouching")); + pSoldier->aiData.usActionData = ANIM_CROUCH; + pSoldier->sniper = 0; + return(AI_ACTION_CHANGE_STANCE); + } + } + + //////////////////////////////////////////////////////////////////////////// + // SNIPER - RAISE WEAPON TO SCAN AREA + //////////////////////////////////////////////////////////////////////////// + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Snipers like to raise weapons, sniper = %d", pSoldier->sniper)); + if (pSoldier->aiData.bOrders == SNIPER && pSoldier->sniper == 0 && (pSoldier->pathing.bLevel == 1 || Random(100) < 40) && (pSoldier->bBreath > 30 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 20)) + { + if (!WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) + { + if (!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, READY_RIFLE_CROUCH) <= pSoldier->bActionPoints) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Sniper is raising weapon, soldier = %d, sniper = %d", pSoldier->ubID, pSoldier->sniper)); + pSoldier->sniper = 1; + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Sniper = %d", pSoldier->sniper)); + return(AI_ACTION_RAISE_GUN); + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // SANDRO - occasionally, allow regular soldiers to scan around too + if (IsScoped(&pSoldier->inv[HANDPOS])) + { + if (!WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) + { + if ((!gfTurnBasedAI || ((GetAPsToReadyWeapon(pSoldier, PickSoldierReadyAnimation(pSoldier, FALSE, FALSE))) <= pSoldier->bActionPoints)) && + (pSoldier->bBreath > 30 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 20)) + { + INT32 iChance = 25; + if (pSoldier->ubSoldierClass == SOLDIER_CLASS_ELITE_MILITIA || pSoldier->ubSoldierClass == SOLDIER_CLASS_ELITE) + iChance += 15; + else if (pSoldier->ubSoldierClass == SOLDIER_CLASS_GREEN_MILITIA || pSoldier->ubSoldierClass == SOLDIER_CLASS_ADMINISTRATOR || pSoldier->ubSoldierClass == SOLDIER_CLASS_BANDIT) + iChance -= 15; + if (Random(100) < iChance) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Soldier deciding to raise weapon with scope")); + return(AI_ACTION_RAISE_GUN); + } + } + } + else // if the weapon is ready already, maybe unready it + { + INT32 iChance = 30; + // is it a heavy gun? And we have energy cost for shooting enabled? + iChance += GetBPCostPer10APsForGunHolding(pSoldier); // don't overexagerate yourself + if (Random(100) < iChance) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Soldier deciding to lower weapon")); + return(AI_ACTION_LOWER_GUN); + } + } + } + //////////////////////////////////////////////////////////////////////////// + + + //////////////////////////////////////////////////////////////////////////// + // LOOK AROUND: determine %chance for man to turn in place + //////////////////////////////////////////////////////////////////////////// + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Soldier deciding to turn")); + if (!gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) + { + // avoid 2 consecutive random turns in a row + if (pSoldier->aiData.bLastAction != AI_ACTION_CHANGE_FACING) + { + INT32 iChance = 25 + pSoldier->aiData.bBypassToGreen; + + // set base chance according to orders + if (pSoldier->aiData.bOrders == STATIONARY || pSoldier->aiData.bOrders == SNIPER) + iChance += 25; + + if (pSoldier->aiData.bOrders == ONGUARD) + iChance += 20; + + if (pSoldier->aiData.bAttitude == DEFENSIVE) + iChance += 25; + + if (pSoldier->aiData.bOrders == SNIPER && pSoldier->pathing.bLevel == 1) + iChance += 35; + + if (WeaponReady(pSoldier)) // SANDRO - if readied weapon, make him more likely to turn around + iChance += 30; + + if ((INT16)PreRandom(100) < iChance) + { + // roll random directions (stored in actionData) until different from current + do + { + // if man has a LEGAL dominant facing, and isn't facing it, he will turn + // back towards that facing 50% of the time here (normally just enemies) + if ((pSoldier->aiData.bDominantDir >= 0) && (pSoldier->aiData.bDominantDir <= 8) && + (pSoldier->ubDirection != pSoldier->aiData.bDominantDir) && PreRandom(2) && pSoldier->aiData.bOrders != SNIPER) + { + pSoldier->aiData.usActionData = pSoldier->aiData.bDominantDir; + } + else + { + INT32 iNoiseValue; + BOOLEAN fClimb; + BOOLEAN fReachable; + INT32 sNoiseGridNo = MostImportantNoiseHeard(pSoldier, &iNoiseValue, &fClimb, &fReachable); + UINT8 ubNoiseDir; + + if (TileIsOutOfBounds(sNoiseGridNo) || + (ubNoiseDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sNoiseGridNo)) == pSoldier->ubDirection) + + { + pSoldier->aiData.usActionData = PreRandom(8); + } + else + { + pSoldier->aiData.usActionData = ubNoiseDir; + } + } + } while (pSoldier->aiData.usActionData == pSoldier->ubDirection); + + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Trying to turn - checking stance validity, sniper = %d", pSoldier->sniper)); + if (pSoldier->InternalIsValidStance((INT8)pSoldier->aiData.usActionData, gAnimControl[pSoldier->usAnimState].ubEndHeight)) + { + + if (!gfTurnBasedAI) + { + // wait after this... + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = RealtimeDelay(pSoldier); + } + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionGreen: Soldier is turning")); + return(AI_ACTION_CHANGE_FACING); + } + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // NONE: + //////////////////////////////////////////////////////////////////////////// + + // by default, if everything else fails, just stands in place without turning + // for realtime, regular AI guys will use a standard wait set outside of here + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); +} + +INT8 DecideActionYellowSoldier(SOLDIERTYPE* pSoldier) +{ + DebugAI(AI_MSG_START, pSoldier, String("[Yellow Soldier]")); + LogDecideInfo(pSoldier); + + INT32 iChance, iSneaky; + + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // sevenfm: initialize data + pSoldier->bWeaponMode = WM_NORMAL; + + if ((gGameExternalOptions.fAllNamedNpcsDecideAction && pSoldier->ubProfile != NO_PROFILE)) + { + if (pSoldier->flags.uiStatusFlags & SOLDIER_COWERING) + { + // everything's peaceful again, stop cowering!! + pSoldier->aiData.usActionData = ANIM_STAND; + return(AI_ACTION_STOP_COWERING); + } + if (!gfTurnBasedAI) + { + // ****************** + // REAL TIME NPC CODE + // ****************** + if (pSoldier->ubProfile != NO_PROFILE || pSoldier->IsAssassin()) + { + if (pSoldier->ubProfile != NO_PROFILE) + pSoldier->aiData.bAction = DecideActionNamedNPC(pSoldier); + else + { + INT32 sDesiredMercDist; + INT32 sDesiredMercLoc = ClosestUnDisguisedPC(pSoldier, &sDesiredMercDist); + + // Flugente: if this guy is disguised, do not consider him + + if (!TileIsOutOfBounds(sDesiredMercLoc)) + { + if (sDesiredMercDist <= NPC_TALK_RADIUS * 2) + { + AddToShouldBecomeHostileOrSayQuoteList(pSoldier->ubID); + // now wait a bit! + pSoldier->aiData.usActionData = 5000; + pSoldier->aiData.bAction = AI_ACTION_WAIT; + } + else + { + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sDesiredMercLoc, AI_ACTION_APPROACH_MERC); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + pSoldier->aiData.bAction = AI_ACTION_APPROACH_MERC; + } + } + } + } + + if (pSoldier->aiData.bAction != AI_ACTION_NONE) + { + return(pSoldier->aiData.bAction); + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // WHEN IN GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND + //////////////////////////////////////////////////////////////////////////// + + if (InGas(pSoldier, pSoldier->sGridNo) || DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel) || FindBombNearby(pSoldier, pSoldier->sGridNo, BOMB_DETECTION_RANGE)) + { + pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_LEAVE_WATER_GAS); + } + } + + // determine the most important noise heard, and its relative value + INT32 iNoiseValue; + BOOLEAN fClimb; + BOOLEAN fReachable; + INT32 sNoiseGridNo = MostImportantNoiseHeard(pSoldier, &iNoiseValue, &fClimb, &fReachable); + //NumMessage("iNoiseValue = ",iNoiseValue); + + if (TileIsOutOfBounds(sNoiseGridNo)) + { + // then we have no business being under YELLOW status any more! + return(AI_ACTION_NONE); + } + + if (gGameExternalOptions.bNewTacticalAIBehavior) + { + //////////////////////////////////////////////////////////////////////////// + // IF YOU SEE CAPTURED FRIENDS, FREE THEM! + //////////////////////////////////////////////////////////////////////////// + + // Flugente: if we see one of our buddies captured, it is a clear sign of enemy activity! + if (gGameExternalOptions.fAllowPrisonerSystem && pSoldier->bTeam == ENEMY_TEAM) + { + SoldierID ubPerson = GetClosestFlaggedSoldierID(pSoldier, 20, ENEMY_TEAM, SOLDIER_POW, TRUE); + + if (ubPerson != NOBODY) + { + // if we are close, we can release this guy + // possible only if not handcuffed (binders can be opened, handcuffs not) + if (!HasItemFlag(ubPerson->inv[HANDPOS].usItem, HANDCUFFS)) + { + if (PythSpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) < 2) + { + // see if we are facing this person + UINT8 ubDesiredMercDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, ubPerson->sGridNo); + + // if not already facing in that direction, + if (pSoldier->ubDirection != ubDesiredMercDir) + { + pSoldier->aiData.usActionData = ubDesiredMercDir; + + return(AI_ACTION_CHANGE_FACING); + } + + return(AI_ACTION_FREE_PRISONER); + } + else + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_SEEK_FRIEND); + } + } + } + else if (!(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT) && !gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition) + { + // raise alarm! + return(AI_ACTION_RED_ALERT); + } + } + } + + // if we are a doctor with medical gear, we might be able to help a wounded ally + if (pSoldier->CanMedicAI()) + { + SoldierID ubPerson = GetClosestWoundedSoldierID(pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius, pSoldier->bTeam); + + // are we ourselves the patient? + if (ubPerson == pSoldier->ubID) + { + // if not already crouched, crouch down first + if (gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_CROUCH && IsValidStance(pSoldier, ANIM_CROUCH) && GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints) + { + pSoldier->aiData.usActionData = ANIM_CROUCH; + + return(AI_ACTION_CHANGE_STANCE); + } + + return(AI_ACTION_DOCTOR_SELF); + } + else if (ubPerson != NOBODY) + { + if (PythSpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) < 2) + { + // see if we are facing this person + UINT8 ubDesiredMercDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, ubPerson->sGridNo); + + // if not already facing in that direction, + if (pSoldier->ubDirection != ubDesiredMercDir) + { + pSoldier->aiData.usActionData = ubDesiredMercDir; + + return(AI_ACTION_CHANGE_FACING); + } + + // if not already crouched, crouch down first + if (gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_CROUCH && IsValidStance(pSoldier, ANIM_CROUCH) && GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints) + { + pSoldier->aiData.usActionData = ANIM_CROUCH; + + return(AI_ACTION_CHANGE_STANCE); + } + + return(AI_ACTION_DOCTOR); + } + else + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + // if we are not a medic, but are wounded, seek a medic + else if (pSoldier->iHealableInjury >= gGameExternalOptions.sEnemyMedicsWoundMinAmount) + { + SoldierID ubPerson = GetClosestMedicSoldierID(pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius / 2, pSoldier->bTeam); + + if (ubPerson != NOBODY) + { + if (PythSpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) > 1) + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + + // are we a bodyguard? + if (pSoldier->usSoldierFlagMask & SOLDIER_BODYGUARD) + { + // is VIP still alive? + SoldierID ubPerson = GetClosestFlaggedSoldierID(pSoldier, 100, pSoldier->bTeam, SOLDIER_VIP, FALSE); + + if (ubPerson != NOBODY) + { + // we want to stay close to him, but still be able to function properly... stay withing a 7-tile radius + if (SpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) > 7) + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // LOOK AROUND TOWARD NOISE: determine %chance for man to turn towards noise + //////////////////////////////////////////////////////////////////////////// + + // determine direction from this soldier in which the noise lies + UINT8 ubNoiseDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sNoiseGridNo); + + // if soldier is not already facing in that direction, + // and the noise source is close enough that it could possibly be seen + if (!gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) + { + if ((pSoldier->ubDirection != ubNoiseDir) && PythSpacesAway(pSoldier->sGridNo, sNoiseGridNo) <= pSoldier->GetMaxDistanceVisible(sNoiseGridNo)) + { + // set base chance according to orders + if ((pSoldier->aiData.bOrders == STATIONARY) || (pSoldier->aiData.bOrders == ONGUARD)) + iChance = 50; + else // all other orders + iChance = 25; + + if (pSoldier->aiData.bAttitude == DEFENSIVE) + iChance += 15; + + + if ((INT16)PreRandom(100) < iChance && pSoldier->InternalIsValidStance(ubNoiseDir, gAnimControl[pSoldier->usAnimState].ubEndHeight)) + { + pSoldier->aiData.usActionData = ubNoiseDir; + + if (pSoldier->aiData.bOrders == SNIPER && + (pSoldier->bBreath > 25 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 30) && + !WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) + { + if (!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, READY_RIFLE_CROUCH) <= pSoldier->bActionPoints) + { + pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; + } + } + //////////////////////////////////////////////////////////////////////////// + // SANDRO - allow regular soldiers to raise scoped weapons to see farther away too + if (IsScoped(&pSoldier->inv[HANDPOS])) + { + if (!WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION && + (pSoldier->bBreath > 25 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 30)) + { + if (!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, PickSoldierReadyAnimation(pSoldier, FALSE, FALSE)) <= pSoldier->bActionPoints) + { + if (Random(100) < 35) + { + pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; + } + } + } + } + //////////////////////////////////////////////////////////////////////////// + + return(AI_ACTION_CHANGE_FACING); + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // RADIO YELLOW ALERT: determine %chance to call others and report noise + //////////////////////////////////////////////////////////////////////////// + + // if we have the action points remaining to RADIO + // (we never want NPCs to choose to radio if they would have to wait a turn) + if (pSoldier->bActionPoints >= APBPConstants[AP_RADIO] && + (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1)) + { + // base chance depends on how much new info we have to radio to the others + iChance = 5 * WhatIKnowThatPublicDont(pSoldier, FALSE); // use 5 * for YELLOW alert + + // if I actually know something they don't and I ain't swimming (deep water) + if (iChance && !DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel)) + { + + // CJC: this addition allows for varying difficulty levels for soldier types + iChance += gbDiff[DIFF_RADIO_RED_ALERT][SoldierDifficultyLevel(pSoldier)] / 2; + + // Alex: this addition replaces the sectorValue/2 in original JA + //iChance += gsDiff[DIFF_RADIO_RED_ALERT][GameOption[ENEMYDIFFICULTY]] / 2; + + // modify base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += 20; break; + case ONGUARD: iChance += 15; break; + case ONCALL: iChance += 10; break; + case CLOSEPATROL: break; + case RNDPTPATROL: + case POINTPATROL: break; + case FARPATROL: iChance += -10; break; + case SEEKENEMY: iChance += -20; break; + case SNIPER: iChance += -10; break; //Madd: sniper contacts are supposed to be automatically reported + } + + // modify base chance according to attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += 20; break; + case BRAVESOLO: iChance += -10; break; + case BRAVEAID: break; + case CUNNINGSOLO:iChance += -5; break; + case CUNNINGAID: break; + case AGGRESSIVE: iChance += -20; break; + case ATTACKSLAYONLY: iChance = 0; break; + } + + + if ((INT16)PreRandom(100) < iChance) + { + return(AI_ACTION_YELLOW_ALERT); + } + } + } + + if (!gGameExternalOptions.fEnemyTanksCanMoveInTactical && ARMED_VEHICLE(pSoldier)) + { + return(AI_ACTION_NONE); + } + + //////////////////////////////////////////////////////////////////////// + // REST IF RUNNING OUT OF BREATH + //////////////////////////////////////////////////////////////////////// + + // if our breath is running a bit low, and we're not in water + if ((pSoldier->bBreath < 25) && !pSoldier->MercInWater()) + { + // take a breather for gods sake! + pSoldier->aiData.usActionData = NOWHERE; + + // is it a heavy gun? And we have energy cost for shooting enabled? + if (WeaponReady(pSoldier) && GetBPCostPer10APsForGunHolding(pSoldier) > 0) + { + // unready + return(AI_ACTION_LOWER_GUN); + } + + return(AI_ACTION_NONE); + } + + //continue flanking + INT32 sFlankGridNo; + + if (TileIsOutOfBounds(sNoiseGridNo)) + sFlankGridNo = pSoldier->lastFlankSpot; + else + sFlankGridNo = sNoiseGridNo; + + if (pSoldier->numFlanks > 0 && pSoldier->numFlanks < MAX_FLANKS_YELLOW) + { + INT16 currDir = GetDirectionFromGridNo(sFlankGridNo, pSoldier); + INT16 origDir = pSoldier->origDir; + pSoldier->numFlanks += 1; + if (pSoldier->flags.lastFlankLeft) + { + if (origDir > currDir) + origDir -= NUM_WORLD_DIRECTIONS; + + // stop flanking if reached desired direction + if ((currDir - origDir) >= MinFlankDirections(pSoldier)) + { + pSoldier->numFlanks = MAX_FLANKS_YELLOW; + } + else + { + pSoldier->aiData.usActionData = FindFlankingSpot(pSoldier, sFlankGridNo, AI_ACTION_FLANK_LEFT); + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) //&& (currDir - origDir) < 2 ) + return AI_ACTION_FLANK_LEFT; + else + pSoldier->numFlanks = MAX_FLANKS_YELLOW; + } + } + else + { + if (origDir < currDir) + origDir += NUM_WORLD_DIRECTIONS; + + // stop flanking if reached desired direction + if ((origDir - currDir) >= MinFlankDirections(pSoldier)) + { + pSoldier->numFlanks = MAX_FLANKS_YELLOW; + } + else + { + pSoldier->aiData.usActionData = FindFlankingSpot(pSoldier, sFlankGridNo, AI_ACTION_FLANK_RIGHT); + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))//&& (origDir - currDir) < 2 ) + return AI_ACTION_FLANK_RIGHT; + else + pSoldier->numFlanks = MAX_FLANKS_YELLOW; + } + } + } + + if (pSoldier->numFlanks == MAX_FLANKS_YELLOW) + { + pSoldier->numFlanks += 1; + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sFlankGridNo, AI_ACTION_SEEK_NOISE); + return AI_ACTION_SEEK_NOISE; + } + + // Hmmm, I don't think this check is doing what is intended. But then I see no comment about what is intended. + // However, civilians with no profile (and likely no weapons) do not need to be seeking out noises. Most don't + // even have the body type for it (can't climb or jump). + //if ( !( pSoldier->bTeam == CIV_TEAM && pSoldier->ubProfile != NO_PROFILE && pSoldier->ubProfile != ELDIN ) ) + //if ( pSoldier->bTeam != CIV_TEAM || ( !pSoldier->aiData.bNeutral && pSoldier->ubProfile != ELDIN ) ) + // ADB: Eldin is the only neutral civilian who should be seeking out noises. As the museum curator, he can be + // available to talk to. As the night watchman, he needs to look for thieves. + bool onCivTeam = (pSoldier->bTeam == CIV_TEAM); + bool isNamedCiv = (pSoldier->ubProfile != NO_PROFILE); + // For purpose of seeking noise, cowardly civs are neutral, even if attacked by your thugs + bool isNeutral = pSoldier->aiData.bNeutral || pSoldier->flags.uiStatusFlags & SOLDIER_COWERING; + if ( + (onCivTeam == false) || //true #1 + (onCivTeam == true && isNamedCiv == true && isNeutral == false) //true #2 + ) + { + // IF WE ARE MILITIA/CIV IN REALTIME, CLOSE TO NOISE, AND CAN SEE THE SPOT WHERE THE NOISE CAME FROM, FORGET IT + if (fReachable && !fClimb && !gfTurnBasedAI && (pSoldier->bTeam == MILITIA_TEAM || pSoldier->bTeam == CIV_TEAM) && PythSpacesAway(pSoldier->sGridNo, sNoiseGridNo) < 5) + { + if (SoldierTo3DLocationLineOfSightTest(pSoldier, sNoiseGridNo, pSoldier->pathing.bLevel, 0, TRUE, 6)) + { + // set reachable to false so we don't investigate + fReachable = FALSE; + // forget about noise + pSoldier->aiData.sNoiseGridno = NOWHERE; + pSoldier->aiData.ubNoiseVolume = 0; + } + } + + //////////////////////////////////////////////////////////////////////////// + // SEEK NOISE + //////////////////////////////////////////////////////////////////////////// + + if (fReachable) + { + // remember that noise value is negative, and closer to 0 => more important! + iChance = 95 + (iNoiseValue / 3); + iSneaky = 30; + + // increase + + // set base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += -20; break; + case ONGUARD: iChance += -15; break; + case ONCALL: break; + case CLOSEPATROL: iChance += -10; break; + case RNDPTPATROL: + case POINTPATROL: break; + case FARPATROL: iChance += 10; break; + case SEEKENEMY: iChance += 25; break; + case SNIPER: iChance += -10; break; + } + + // modify chance of patrol (and whether it's a sneaky one) by attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += -10; iSneaky += 15; break; + case BRAVESOLO: iChance += 10; break; + case BRAVEAID: iChance += 5; break; + case CUNNINGSOLO: iChance += 5; iSneaky += 30; break; + case CUNNINGAID: iSneaky += 30; break; + case AGGRESSIVE: iChance += 20; iSneaky += -10; break; + case ATTACKSLAYONLY: iChance += 20; iSneaky += -10; break; + } + + + // reduce chance if breath is down, less likely to wander around when tired + iChance -= (100 - pSoldier->bBreath); + + //Madd: make militia less likely to go running headlong into trouble + if (pSoldier->bTeam == MILITIA_TEAM) + iChance -= 30; + + if ((INT16)PreRandom(100) < iChance) + { + + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sNoiseGridNo, AI_ACTION_SEEK_NOISE); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - INVESTIGATING NOISE at grid %d, moving to %d", + pSoldier->name, sNoiseGridNo, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + if (fClimb)//&& pSoldier->aiData.usActionData == sNoiseGridNo) + { + // need to climb AND have enough APs to get there this turn + BOOLEAN fUp = TRUE; + if (pSoldier->pathing.bLevel > 0) + fUp = FALSE; + + if (!fUp) + DebugMsg(TOPIC_JA2AI, DBG_LEVEL_3, String("Soldier %d, is climbing down", pSoldier->ubID)); + + // 0verhaul: the Closest Noise call returns the location of a climb. So 1) it's not necessary to + // ask if we can climb from here. And 2) It's not necessary to look for the climb point. We already + // have it. +// if ( CanClimbFromHere ( pSoldier, fUp ) ) + if (pSoldier->sGridNo == sNoiseGridNo) + { + if (IsActionAffordable(pSoldier) && pSoldier->bActionPoints >= (APBPConstants[AP_CLIMBROOF] + MinAPsToAttack(pSoldier, sNoiseGridNo, ADDTURNCOST, 0))) + { + return(AI_ACTION_CLIMB_ROOF); + } + } + else + { + // pSoldier->aiData.usActionData = FindClosestClimbPoint(pSoldier, pSoldier->sGridNo , sNoiseGridNo , fUp ); + pSoldier->aiData.usActionData = sNoiseGridNo; + //if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_MOVE_TO_CLIMB); + } + } + } + + // possibly start YELLOW flanking + if (gGameExternalOptions.fAIYellowFlanking && + (pSoldier->aiData.bAttitude == CUNNINGAID || pSoldier->aiData.bAttitude == CUNNINGSOLO) && + pSoldier->bTeam == ENEMY_TEAM && + (CountFriendsInDirection(pSoldier, sNoiseGridNo) > 0 || NightTime()) && + (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); + if (dist > MIN_FLANK_DIST_YELLOW && dist < MAX_FLANK_DIST_YELLOW) + { + INT16 rdm = Random(6); + + switch (rdm) + { + case 1: + case 2: + case 3: + if (pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT) + action = AI_ACTION_FLANK_LEFT; + break; + default: + if (pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT) + action = AI_ACTION_FLANK_RIGHT; + break; + } + } + else + return AI_ACTION_SEEK_NOISE; + + pSoldier->aiData.usActionData = FindFlankingSpot(pSoldier, sNoiseGridNo, action); + + if (TileIsOutOfBounds(pSoldier->aiData.usActionData) || pSoldier->numFlanks >= MAX_FLANKS_YELLOW) + { + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sNoiseGridNo, AI_ACTION_SEEK_NOISE); + //pSoldier->numFlanks = 0; + return(AI_ACTION_SEEK_NOISE); + } + else + { + if (action == AI_ACTION_FLANK_LEFT) + pSoldier->flags.lastFlankLeft = TRUE; + else + pSoldier->flags.lastFlankLeft = FALSE; + + if (pSoldier->lastFlankSpot != sNoiseGridNo) + pSoldier->numFlanks = 0; + + pSoldier->origDir = GetDirectionFromGridNo(sNoiseGridNo, pSoldier); + pSoldier->lastFlankSpot = sNoiseGridNo; + pSoldier->numFlanks++; + + // sevenfm: change orders CLOSEPATROL -> FARPATROL + if (pSoldier->aiData.bOrders == CLOSEPATROL) + { + pSoldier->aiData.bOrders = FARPATROL; + } + + return(action); + } + } + else + { + return(AI_ACTION_SEEK_NOISE); + } + + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // SEEK FRIEND WHO LAST RADIOED IN TO REPORT NOISE + //////////////////////////////////////////////////////////////////////////// + + INT32 sClosestFriend = ClosestReachableFriendInTrouble(pSoldier, &fClimb); + + // if there is a friend alive & reachable who last radioed in + if (!TileIsOutOfBounds(sClosestFriend)) + { + // there a chance enemy soldier choose to go "help" his friend + iChance = 50 - SpacesAway(pSoldier->sGridNo, sClosestFriend); + iSneaky = 10; + + // set base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += -20; break; + case ONGUARD: iChance += -15; break; + case ONCALL: iChance += 20; break; + case CLOSEPATROL: iChance += -10; break; + case RNDPTPATROL: + case POINTPATROL: iChance += -10; break; + case FARPATROL: break; + case SEEKENEMY: iChance += 10; break; + case SNIPER: iChance += -10; break; + } + + // modify chance of patrol (and whether it's a sneaky one) by attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += -10; iSneaky += 15; break; + case BRAVESOLO: break; + case BRAVEAID: iChance += 20; iSneaky += -10; break; + case CUNNINGSOLO: iSneaky += 30; break; + case CUNNINGAID: iChance += 20; iSneaky += 20; break; + case AGGRESSIVE: iChance += -20; iSneaky += -20; break; + case ATTACKSLAYONLY: iChance += -20; iSneaky += -20; break; + } + + // reduce chance if breath is down, less likely to wander around when tired + iChance -= (100 - pSoldier->bBreath); + + if ((INT16)PreRandom(100) < iChance) + { + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sClosestFriend, AI_ACTION_SEEK_FRIEND); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - SEEKING FRIEND at %d, MOVING to %d", + pSoldier->name, sClosestFriend, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + if (fClimb)//&& pSoldier->aiData.usActionData == sClosestFriend) + { + // need to climb AND have enough APs to get there this turn + BOOLEAN fUp = TRUE; + if (pSoldier->pathing.bLevel > 0) + fUp = FALSE; + + if (!fUp) + DebugMsg(TOPIC_JA2AI, DBG_LEVEL_3, String("Soldier %d is climbing down", pSoldier->ubID)); + + // 0verhaul: Closest Friend call also returns the climb point if climbing is necessary. So don't + // climb the wrong building and don't search again + //if ( CanClimbFromHere ( pSoldier, fUp ) ) + if (pSoldier->sGridNo == sClosestFriend) + { + if (IsActionAffordable(pSoldier)) + { + return(AI_ACTION_CLIMB_ROOF); + } + } + else + { + //pSoldier->aiData.usActionData = FindClosestClimbPoint(pSoldier, pSoldier->sGridNo , sClosestFriend , fUp ); + pSoldier->aiData.usActionData = sClosestFriend; + //if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_MOVE_TO_CLIMB); + } + } + } + + //if (fClimb && pSoldier->aiData.usActionData == sClosestFriend) + //{ + //// need to climb AND have enough APs to get there this turn + //return( AI_ACTION_CLIMB_ROOF ); + //} + + return(AI_ACTION_SEEK_FRIEND); + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // TAKE BEST NEARBY COVER FROM THE NOISE GENERATING GRIDNO + //////////////////////////////////////////////////////////////////////////// + + if (!SkipCoverCheck) // && gfTurnBasedAI) // only do in turnbased + { + // remember that noise value is negative, and closer to 0 => more important! + iChance = 25; + iSneaky = 30; + + // set base chance according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: iChance += 20; break; + case ONGUARD: iChance += 15; break; + case ONCALL: break; + case CLOSEPATROL: iChance += 10; break; + case RNDPTPATROL: + case POINTPATROL: break; + case FARPATROL: iChance += -5; break; + case SEEKENEMY: iChance += -20; break; + case SNIPER: iChance += 20; break; + } + + // modify chance (and whether it's sneaky) by attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iChance += 10; iSneaky += 15; break; + case BRAVESOLO: iChance += -15; iSneaky += -20; break; + case BRAVEAID: iChance += -20; iSneaky += -20; break; + case CUNNINGSOLO: iChance += 20; iSneaky += 30; break; + case CUNNINGAID: iChance += 15; iSneaky += 30; break; + case AGGRESSIVE: iChance += -10; iSneaky += -10; break; + case ATTACKSLAYONLY: iChance += -10; iSneaky += -10; break; + } + + + //Madd: make militia more likely to take cover + if (pSoldier->bTeam == MILITIA_TEAM) + iChance += 20; + + // reduce chance if breath is down, less likely to wander around when tired + iChance -= (100 - pSoldier->bBreath); + + if ((INT16)PreRandom(100) < iChance) + { + INT32 iDummy; + pSoldier->aiData.bAIMorale = CalcMorale(pSoldier); + pSoldier->aiData.usActionData = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iDummy); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - TAKING COVER at grid %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + return(AI_ACTION_TAKE_COVER); + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // SWITCH TO GREEN: determine if soldier acts as if nothing at all was wrong + //////////////////////////////////////////////////////////////////////////// + if ((INT16)PreRandom(100) < 50) + { +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "ignores noise completely and BYPASSES to GREEN!", 1000); +#endif + // Skip YELLOW until new situation, 15% extra chance to do GREEN actions + pSoldier->aiData.bBypassToGreen = 15; + return(DecideActionGreenSoldier(pSoldier)); + } + + + //////////////////////////////////////////////////////////////////////////// + // CROUCH IF NOT CROUCHING ALREADY + //////////////////////////////////////////////////////////////////////////// + + // if not in water and not already crouched, try to crouch down first + if (!PTR_CROUCHED && IsValidStance(pSoldier, ANIM_CROUCH)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s CROUCHES (STATUS YELLOW)", pSoldier->name); + AIPopMessage(tempstr); +#endif + + if (!gfTurnBasedAI || GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints) + { + //////////////////////////////////////////////////////////////////////////// + // SANDRO - raise weapon maybe + if (!WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION && + pSoldier->ubDirection == ubNoiseDir && // if we are facing the direction of where the noise came from + (pSoldier->bBreath > 25 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 30)) + { + if (!gfTurnBasedAI || (((GetAPsToReadyWeapon(pSoldier, PickSoldierReadyAnimation(pSoldier, FALSE, FALSE))) + GetAPsToChangeStance(pSoldier, ANIM_CROUCH)) <= pSoldier->bActionPoints)) + { + if (IsScoped(&pSoldier->inv[HANDPOS])) + { + pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; + } + } + } + //////////////////////////////////////////////////////////////////////////// + + pSoldier->aiData.usActionData = ANIM_CROUCH; + return(AI_ACTION_CHANGE_STANCE); + } + } + else + { + //////////////////////////////////////////////////////////////////////////// + // SANDRO - raise weapon maybe + if (!WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION && + pSoldier->ubDirection == ubNoiseDir && // if we are facing the direction of where the noise came from + (pSoldier->bBreath > 25 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 30)) + { + if (!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, pSoldier->usAnimState) <= pSoldier->bActionPoints) + { + if (IsScoped(&pSoldier->inv[HANDPOS])) + { + if (Random(100) < 35) + { + return(AI_ACTION_RAISE_GUN); + } + } + } + } + //////////////////////////////////////////////////////////////////////////// + } + + + //////////////////////////////////////////////////////////////////////////// + // DO NOTHING: Not enough points left to move, so save them for next turn + //////////////////////////////////////////////////////////////////////////// + +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "- DOES NOTHING (YELLOW)", 1000); +#endif + + // by default, if everything else fails, just stands in place without turning + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); +} + + +INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) +{ + DebugAI(AI_MSG_START, pSoldier, String("[Red Soldier]"), gLogDecideActionRed); + LogDecideInfo(pSoldier, gLogDecideActionRed); + + auto decision = AI_ACTION_INVALID; + + INT32 iDummy; + INT32 iChance; + INT32 sClosestDisturbance = NOWHERE, sCheckGridNo; + INT32 sDistVisible; + UINT8 ubCanMove, ubOpponentDir; + INT8 bSeekPts = 0, bHelpPts = 0, bHidePts = 0, bWatchPts = 0; + INT8 bHighestWatchLoc; + ATTACKTYPE BestThrow, BestShot; + + BOOLEAN fClimb; + + + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // sevenfm: initialize data + pSoldier->bWeaponMode = WM_NORMAL; + + // if we have absolutely no action points, we can't do a thing under RED! + if (pSoldier->bActionPoints <= 0) //Action points can be negative + { + pSoldier->aiData.usActionData = NOWHERE; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_NONE); + } + + // sevenfm: find closest opponent + INT32 sOpponentGridNo; + INT8 bOpponentLevel; + INT32 distanceToOpponent; + const INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel, NULL, &distanceToOpponent); + DebugAI(AI_MSG_INFO, pSoldier, String("sClosestOpponent %d", sClosestOpponent), gLogDecideActionRed); + + BOOLEAN fCanBeSeen = FALSE; + if (!SightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE)) + { + fCanBeSeen = TRUE; + DebugAI(AI_MSG_INFO, pSoldier, String("can be seen"), gLogDecideActionRed); + } + + BOOLEAN fProneSightCover = ProneSightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE); + BOOLEAN fAnyCover = AnyCoverAtSpot(pSoldier, pSoldier->sGridNo); + DebugAI(AI_MSG_INFO, pSoldier, String("prone sight cover %d", fProneSightCover), gLogDecideActionRed); + DebugAI(AI_MSG_INFO, pSoldier, String("any cover %d", fAnyCover), gLogDecideActionRed); + + BOOLEAN fDangerousSpot = FALSE; + if (!fProneSightCover || pSoldier->aiData.bUnderFire) + { + fDangerousSpot = TRUE; + } + + // can this guy move to any of the neighbouring squares ? (sets TRUE/FALSE) + ubCanMove = (pSoldier->bActionPoints >= MinPtsToMove(pSoldier)); + + // sevenfm: before deciding anything, stop cowering + if (ubCanMove && + pSoldier->stats.bLife >= OKLIFE && + !pSoldier->bCollapsed && + !pSoldier->bBreathCollapsed && + pSoldier->IsCowering()) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop cowering"), gLogDecideActionRed); + return AI_ACTION_STOP_COWERING; + } + + // sevenfm: stop giving aid + if (pSoldier->bActionPoints > 0 && + pSoldier->stats.bLife >= OKLIFE && + !pSoldier->bCollapsed && + !pSoldier->bBreathCollapsed && + pSoldier->IsGivingAid()) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop giving aid"), gLogDecideActionRed); + return AI_ACTION_STOP_MEDIC; + } + + // if we're an alerted enemy, and there are panic bombs or a trigger around + if ((!PTR_CIVILIAN || pSoldier->ubProfile == WARDEN) && ((gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition || (pSoldier->ubID == gTacticalStatus.ubTheChosenOne) || (pSoldier->ubProfile == WARDEN)) && + (gTacticalStatus.fPanicFlags & (PANIC_BOMBS_HERE | PANIC_TRIGGERS_HERE)))) + { + if (pSoldier->ubProfile == WARDEN && gTacticalStatus.ubTheChosenOne == NOBODY) + { + PossiblyMakeThisEnemyChosenOne(pSoldier); + } + + // do some special panic AI decision making + decision = PanicAI(pSoldier, ubCanMove); + + // if we decided on an action while in there, we're done + if (decision != AI_ACTION_INVALID) + return(decision); + } + + if (pSoldier->ubProfile != NO_PROFILE) + { + if ((pSoldier->ubProfile == QUEEN || pSoldier->ubProfile == JOE) && ubCanMove) + { + if (gWorldSectorX == 3 && gWorldSectorY == MAP_ROW_P && gbWorldSectorZ == 0 && !gfUseAlternateQueenPosition) + { + ActionType bActionReturned = HeadForTheStairCase(pSoldier); + if (bActionReturned != AI_ACTION_NONE) + { + return(bActionReturned); + } + } + } + } + + + // determine if we happen to be in water (in which case we're in BIG trouble!) + INT8 bInWater = Water(pSoldier->sGridNo, pSoldier->pathing.bLevel); + INT8 bInDeepWater = DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel); + + //////////////////////////////////////////////////////////////////////////// + // WHEN LEFT IN GAS, WEAR GAS MASK IF AVAILABLE AND NOT WORN + //////////////////////////////////////////////////////////////////////////// + INT8 bInGas = DecideActionWearGasmask(pSoldier); + + //////////////////////////////////////////////////////////////////////////// + // WHEN IN GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND + //////////////////////////////////////////////////////////////////////////// + // when in deep water, move to closest opponent + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide action if stuck in water or gas]"), gLogDecideActionRed); + if (ubCanMove && bInDeepWater && !pSoldier->aiData.bNeutral && pSoldier->aiData.bOrders == SEEKENEMY) + { + // find closest reachable opponent, excluding opponents in deep water + pSoldier->aiData.usActionData = ClosestReachableDisturbance(pSoldier, &fClimb); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Move out of water towards closest opponent"), gLogDecideActionRed); + return(AI_ACTION_LEAVE_WATER_GAS); + } + } + + if (ubCanMove && (bInGas || bInDeepWater || FindBombNearby(pSoldier, pSoldier->sGridNo, BOMB_DETECTION_RANGE) || RedSmokeDanger(pSoldier->sGridNo, pSoldier->pathing.bLevel))) + { + pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - SEEKING NEAREST UNGASSED LAND at grid %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + DebugAI(AI_MSG_INFO, pSoldier, String("Leave for nearest (ungassed) land"), gLogDecideActionRed); + return(AI_ACTION_LEAVE_WATER_GAS); + } + } + + + + //////////////////////////////////////////////////////////////////////// + // IF POSSIBLE, FIRE LONG RANGE WEAPONS AT TARGETS REPORTED BY RADIO + //////////////////////////////////////////////////////////////////////// + + // can't do this in realtime, because the player could be shooting a gun or whatever at the same time! + if (gfTurnBasedAI && + !bInWater && + !bInGas && + pSoldier->CheckInitialAP() && + !pSoldier->IsFlanking() && + (CanNPCAttack(pSoldier) == TRUE)) + { + BestThrow.ubPossible = FALSE; // by default, assume Throwing isn't possible + DebugAI(AI_MSG_TOPIC, pSoldier, String("[CheckIfTossPossible]"), gLogDecideActionRed); + CheckIfTossPossible(pSoldier, &BestThrow); + + + //////////////////////////////////////////////////////////////////////// + // CHECK IF THROWING A GRENADE OR USING A LAUNCHER/MORTAR AGAINST ENEMY IS POSSIBLE + //////////////////////////////////////////////////////////////////////// + if (BestThrow.ubPossible) + { + DebugAI(AI_MSG_INFO, pSoldier, String("throw possible"), gLogDecideActionRed); + // sevenfm: allow using mortars, grenade launchers, flares and grenades in RED state + if (ItemIsMortar(pSoldier->inv[BestThrow.bWeaponIn].usItem) || + //Item[pSoldier->inv[ BestThrow.bWeaponIn ].usItem].cannon || + ItemIsRocketLauncher(pSoldier->inv[BestThrow.bWeaponIn].usItem) || + ItemIsGrenadeLauncher(pSoldier->inv[BestThrow.bWeaponIn].usItem) || + ItemIsFlare(pSoldier->inv[BestThrow.bWeaponIn].usItem) || + Item[pSoldier->inv[BestThrow.bWeaponIn].usItem].usItemClass & IC_GRENADE) + { + // if firing mortar make sure we have room + if (ItemIsMortar(pSoldier->inv[BestThrow.bWeaponIn].usItem)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("using mortar, check room to deploy"), gLogDecideActionRed); + ubOpponentDir = AIDirection(pSoldier->sGridNo, BestThrow.sTarget); + + // Get new gridno! + sCheckGridNo = NewGridNo(pSoldier->sGridNo, DirectionInc(ubOpponentDir)); + + if (!OKFallDirection(pSoldier, sCheckGridNo, pSoldier->pathing.bLevel, ubOpponentDir, pSoldier->usAnimState)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("no room to deploy mortar, check if we can move behind"), gLogDecideActionRed); + + // can't fire! + BestThrow.ubPossible = FALSE; + + // try behind us, see if there's room to move back + sCheckGridNo = NewGridNo(pSoldier->sGridNo, DirectionInc(gOppositeDirection[ubOpponentDir])); + if (OKFallDirection(pSoldier, sCheckGridNo, pSoldier->pathing.bLevel, gOppositeDirection[ubOpponentDir], pSoldier->usAnimState)) + { + // sevenfm: check if we can reach this gridno + INT32 iPathCost = EstimatePlotPath(pSoldier, sCheckGridNo, FALSE, FALSE, FALSE, DetermineMovementMode(pSoldier, AI_ACTION_GET_CLOSER), pSoldier->bStealthMode, FALSE, 0); + if (iPathCost != 0 && iPathCost + BestThrow.ubAPCost + GetAPsToLook(pSoldier) + GetAPsCrouch(pSoldier, FALSE) <= pSoldier->bActionPoints) + { + DebugAI(AI_MSG_INFO, pSoldier, String("moving backwards to have more room to deploy mortar"), gLogDecideActionRed); + pSoldier->aiData.usActionData = sCheckGridNo; + + DebugAI(AI_MSG_INFO, pSoldier, String("prepare next action throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime), gLogDecideActionRed); + + // if necessary, swap the usItem + if (BestThrow.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket"), gLogDecideActionRed); + RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); + } + + pSoldier->aiData.usNextActionData = BestThrow.sTarget; + pSoldier->aiData.bNextTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + + pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; + + return AI_ACTION_GET_CLOSER; + } + } + + // can't fire! + BestThrow.ubPossible = FALSE; + } + } + + // if still possible + if (BestThrow.ubPossible) + { + DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime), gLogDecideActionRed); + + // if necessary, swap the usItem + if (BestThrow.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket"), gLogDecideActionRed); + RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); + } + + // sevenfm: correctly set weapon mode for attached GL + if (IsGrenadeLauncherAttached(&pSoldier->inv[HANDPOS])) + { + DebugAI(AI_MSG_INFO, pSoldier, String("set attached GL mode"), gLogDecideActionRed); + pSoldier->bWeaponMode = WM_ATTACHED_GL; + } + + // stand up before throwing if needed + if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && + pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before throw"), gLogDecideActionRed); + pSoldier->aiData.usActionData = BestThrow.ubStance; + pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; + pSoldier->aiData.usNextActionData = BestThrow.sTarget; + pSoldier->aiData.bNextTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + return AI_ACTION_CHANGE_STANCE; + } + else + { + pSoldier->aiData.usActionData = BestThrow.sTarget; + pSoldier->bTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + } + + DebugAI(AI_MSG_INFO, pSoldier, String("Throw grenade / use launcher!"), gLogDecideActionRed); + return(AI_ACTION_TOSS_PROJECTILE); + } + } + } + else // toss/throw/launch not possible + { + DebugAI(AI_MSG_INFO, pSoldier, String("throw not possible"), gLogDecideActionRed); + // WDS - Fix problem when there is no "best thrown" weapon (i.e., BestThrow.bWeaponIn == NO_SLOT) + // if this dude has a longe-range weapon on him (longer than normal + // sight range), and there's at least one other team-mate around, and + // spotters haven't already been called for, then DO SO! + + if ((BestThrow.bWeaponIn != NO_SLOT) && + (CalcMaxTossRange(pSoldier, pSoldier->inv[BestThrow.bWeaponIn].usItem, TRUE) > MaxNormalDistanceVisible()) && + (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1) && + (gTacticalStatus.ubSpottersCalledForBy == NOBODY)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("throw not possible, call for spotters!"), gLogDecideActionRed); + + // then call for spotters! Uses up the rest of his turn (whatever + // that may be), but from now on, BLACK AI NPC may radio sightings! + gTacticalStatus.ubSpottersCalledForBy = pSoldier->ubID; + + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); + } + } + + + //////////////////////////////////////////////////////////////////////// + // THROW SMOKE TO PROVIDE COVER FOR FRIEND + //////////////////////////////////////////////////////////////////////// + if (gfTurnBasedAI && + !bInWater && + !bInGas && + !pSoldier->IsFlanking() && + pSoldier->CheckInitialAP() && + !pSoldier->aiData.bUnderFire && + SightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE) && + !AICheckIsSniper(pSoldier) && + !AICheckIsMachinegunner(pSoldier) && + !AICheckIsMortarOperator(pSoldier) && + Chance(100 - min(100, 10 * CountPublicKnownEnemies(pSoldier, pSoldier->sGridNo, TACTICAL_RANGE))) && + !GuySawEnemy(pSoldier, SEEN_LAST_TURN)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[use smoke to cover friend]"), gLogDecideActionRed); + + CheckTossFriendSmoke(pSoldier, &BestThrow); + + if (BestThrow.ubPossible) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Throw possible"), gLogDecideActionRed); + DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime), gLogDecideActionRed); + + // start retreating for several turns + if (BestThrow.ubOpponent != NOBODY && !BestThrow.ubOpponent->IsFlanking()) + { + DebugAI(AI_MSG_INFO, pSoldier, String("start retreat counter for %d", BestThrow.ubOpponent.i), gLogDecideActionRed); + BestThrow.ubOpponent->RetreatCounterStart(2); + } + + // if necessary, swap the usItem from holster into the hand position + if (BestThrow.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket"), gLogDecideActionRed); + RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); + } + + // stand up before throwing if needed + if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && + pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before throw"), gLogDecideActionRed); + pSoldier->aiData.usActionData = BestThrow.ubStance; + pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; + pSoldier->aiData.usNextActionData = BestThrow.sTarget; + pSoldier->aiData.bNextTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + return AI_ACTION_CHANGE_STANCE; + } + else + { + pSoldier->aiData.usActionData = BestThrow.sTarget; + pSoldier->bTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + } + + DebugAI(AI_MSG_INFO, pSoldier, String("throw smoke grenade to cover friend %d at spot %d level %d", BestThrow.ubOpponent, BestThrow.sTarget, BestThrow.bTargetLevel), gLogDecideActionRed); + + return(AI_ACTION_TOSS_PROJECTILE); + } + } + + + //////////////////////////////////////////////////////////////////////// + // SNIPER / SUPPRESSION + //////////////////////////////////////////////////////////////////////// + // sevenfm: moved can attack check here as only sniper/suppression code needs usable gun + if (CanNPCAttack(pSoldier) == TRUE) + { + // SNIPER! + // sevenfm: set bAimShotLocation + pSoldier->bAimShotLocation = AIM_SHOT_RANDOM; + CheckIfShotPossible(pSoldier, &BestShot); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: is sniper shot possible? = %d, CTH = %d", BestShot.ubPossible, BestShot.ubChanceToReallyHit)); + DebugAI(AI_MSG_INFO, pSoldier, String("Is sniper shot possible? = %d, CTH = %d", BestShot.ubPossible, BestShot.ubChanceToReallyHit), gLogDecideActionRed); + + if (BestShot.ubPossible && BestShot.ubChanceToReallyHit > 50) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Sniper shot possible!"), gLogDecideActionRed); + // then do it! The functions have already made sure that we have a + // pair of worthy opponents, etc., so we're not just wasting our time + + // if necessary, swap the usItem from holster into the hand position + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: sniper shot possible!"); + if (BestShot.bWeaponIn != HANDPOS) + RearrangePocket(pSoldier, HANDPOS, BestShot.bWeaponIn, FOREVER); + + pSoldier->aiData.usActionData = BestShot.sTarget; + //POSSIBLE STRUCTURE CHANGE PROBLEM. GOTTHARD 7/14/08 + pSoldier->aiData.bAimTime = BestShot.ubAimTime; + pSoldier->bScopeMode = BestShot.bScopeMode; + // check if using sniper rifle + if (Weapon[Item[pSoldier->inv[HANDPOS].usItem].ubClassIndex].ubWeaponType == GUN_SN_RIFLE) + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, New113Message[MSG113_SNIPER]); + return(AI_ACTION_FIRE_GUN); + } + else // snipe not possible + { + DebugAI(AI_MSG_INFO, pSoldier, String("Sniper shot NOT possible!"), gLogDecideActionRed); + // if this dude has a long-range weapon on him (longer than normal + // sight range), and there's at least one other team-mate around, and + // spotters haven't already been called for, then DO SO! + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: sniper shot not possible"); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: weapon in slot #%d", BestShot.bWeaponIn)); + // WDS - Fix problem when there is no "best shot" weapon (i.e., BestShot.bWeaponIn == NO_SLOT) + if (BestShot.bWeaponIn != NO_SLOT) { + OBJECTTYPE* gun = &pSoldier->inv[BestShot.bWeaponIn]; + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: men in sector %d, ubspotters called by %d, nobody %d", gTacticalStatus.Team[pSoldier->bTeam].bMenInSector, gTacticalStatus.ubSpottersCalledForBy, NOBODY)); + if (((IsScoped(gun) && GunRange(gun, pSoldier) > MaxNormalDistanceVisible()) || pSoldier->aiData.bOrders == SNIPER) && // SANDRO - added argument + (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1) && + (gTacticalStatus.ubSpottersCalledForBy == NOBODY)) + + { + // then call for spotters! Uses up the rest of his turn (whatever + // that may be), but from now on, BLACK AI NPC may radio sightings! + gTacticalStatus.ubSpottersCalledForBy = pSoldier->ubID; + // HEADROCK HAM 3.1: This may be causing problems with HAM's lowered AP limit. From now on, we'll check + // whether the soldier has more than 0 APs to begin with. + if (pSoldier->bActionPoints > 0) + pSoldier->bActionPoints = 0; + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: calling for sniper spotters"); + DebugAI(AI_MSG_INFO, pSoldier, String("Call for spotters"), gLogDecideActionRed); + + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); + } + } + } + + //SUPPRESSION FIRE + //CheckIfShotPossible(pSoldier, &BestShot); //WarmSteel - No longer returns 0 when there IS actually a chance to hit. + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Suppression decisions]"), gLogDecideActionRed); + + //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 && + pSoldier->bActionPoints > APBPConstants[AP_MINIMUM] && + IsGunAutofireCapable(&pSoldier->inv[BestShot.bWeaponIn]) && + Weapon[pSoldier->inv[BestShot.bWeaponIn].usItem].swapClips && + (!pSoldier->aiData.bUnderFire && !GuySawEnemy(pSoldier, SEEN_LAST_TURN) && (TileIsOutOfBounds(sClosestOpponent) || distanceToOpponent > 10*TACTICAL_RANGE / 2) || AICheckIsMachinegunner(pSoldier) && Chance(25) || Chance(10)) && + pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft < gGameExternalOptions.ubAISuppressionMinimumAmmo && + GetMagSize(&pSoldier->inv[BestShot.bWeaponIn]) >= gGameExternalOptions.ubAISuppressionMinimumMagSize) + // || pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft < (UINT8)(GetMagSize(&pSoldier->inv[BestShot.bWeaponIn]) / 4))) + { + // HEADROCK HAM 5: Fixed an issue where no ammo was found, leading to a crash when overloading the + // inventory vector (bAmmoSlot = -1...) + INT8 bAmmoSlot = FindAmmoToReload(pSoldier, BestShot.bWeaponIn, NO_SLOT); + if (bAmmoSlot > -1) + { + OBJECTTYPE* pAmmo = &(pSoldier->inv[bAmmoSlot]); + if ((*pAmmo)[0]->data.ubShotsLeft > pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft && GetAPsToReloadGunWithAmmo(pSoldier, &(pSoldier->inv[BestShot.bWeaponIn]), pAmmo) <= (INT16)pSoldier->bActionPoints) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Reload weapon"), gLogDecideActionRed); + pSoldier->aiData.usActionData = BestShot.bWeaponIn; + return AI_ACTION_RELOAD_GUN; + } + } + } + + // 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) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Found spare ammo"), gLogDecideActionRed); + fExtraClip = TRUE; + } + } + + // CHRISL: Changed from a simple flag to two externalized values for more modder control over AI suppression + // WarmSteel - Don't *always* try to suppress when under 50 CTH + if (BestShot.ubPossible && + BestShot.bWeaponIn != -1 && + // check valid target + !TileIsOutOfBounds(BestShot.sTarget) && + BestShot.ubOpponent != NOBODY && + Chance(100 - BestShot.ubOpponent->ShockLevelPercent() / 2) && + // check weapon/ammo requirements + IsGunAutofireCapable(&pSoldier->inv[BestShot.bWeaponIn]) && + GetMagSize(&pSoldier->inv[BestShot.bWeaponIn]) >= gGameExternalOptions.ubAISuppressionMinimumMagSize && + pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft >= gGameExternalOptions.ubAISuppressionMinimumAmmo && + // check soldier and weapon + pSoldier->aiData.bOrders != SNIPER && + BestShot.ubFriendlyFireChance <= MIN_CHANCE_TO_ACCIDENTALLY_HIT_SOMEONE && + !AICheckIsFlanking(pSoldier) && + (Chance(BestShot.ubChanceToReallyHit) || Chance(gGameExternalOptions.sSuppressionEffectiveness)) && + (!gGameExternalOptions.fAISafeSuppression || CheckSuppressionDirection(pSoldier, BestShot.sTarget, BestShot.bTargetLevel)) && + !pSoldier->RetreatCounterValue() && + // check cover + (fAnyCover || // safe position + !fCanBeSeen && NightLight() && CountFriendsFlankSameSpot(pSoldier, sClosestOpponent) && Chance(50) || + pSoldier->aiData.bUnderFire && (pSoldier->ubPreviousAttackerID == BestShot.ubOpponent || pSoldier->ubNextToPreviousAttackerID == BestShot.ubOpponent || BestShot.ubOpponent->sLastTarget == pSoldier->sGridNo) || // return fire + Chance((BestShot.ubChanceToReallyHit + 100) / 2) || // 50% chance to fire without cover + //SoldierToSoldierLineOfSightTest(pSoldier, MercPtrs[BestShot.ubOpponent], TRUE, CALC_FROM_ALL_DIRS)) && // can see target after turning + LOS_Raised(pSoldier, BestShot.ubOpponent, CALC_FROM_ALL_DIRS)) && // can see target after turning + // reduce chance to shoot if target is beyond weapon range + (AICheckIsMachinegunner(pSoldier) || + AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || + pSoldier->aiData.bUnderFire && (pSoldier->ubPreviousAttackerID == BestShot.ubOpponent || pSoldier->ubNextToPreviousAttackerID == BestShot.ubOpponent || BestShot.ubOpponent->sLastTarget == pSoldier->sGridNo) || // return fire + Chance(100 * (GunRange(&pSoldier->inv[BestShot.bWeaponIn], pSoldier) / CELL_X_SIZE) / PythSpacesAway(pSoldier->sGridNo, BestShot.sTarget))) && + // check that we have spare ammo + (fExtraClip || pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft >= gGameExternalOptions.ubAISuppressionMinimumMagSize)) + { + // then do it! + + // if necessary, swap the usItem from holster into the hand position + DebugAI(AI_MSG_INFO, pSoldier, String("suppression fire possible! target %d level %d aim %d", BestShot.sTarget, BestShot.bTargetLevel, BestShot.ubAimTime), gLogDecideActionRed); + + if (BestShot.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket"), gLogDecideActionRed); + RearrangePocket(pSoldier, HANDPOS, BestShot.bWeaponIn, FOREVER); + } + + pSoldier->bTargetLevel = BestShot.bTargetLevel; + pSoldier->aiData.bAimTime = BestShot.ubAimTime; + pSoldier->bDoAutofire = 0; + pSoldier->bDoBurst = 1; + pSoldier->bScopeMode = BestShot.bScopeMode; + + INT16 ubBurstAPs = 0; + FLOAT dTotalRecoil = 0; + INT32 sActualAimAP; + UINT8 ubAutoPenalty; + INT16 sReserveAP = GetAPsProne(pSoldier, TRUE); + UINT8 ubMinAuto = 5; + + if (BestShot.ubAimTime > 0 && + !UsingNewCTHSystem() && + Chance((100 - BestShot.ubChanceToReallyHit) * (100 - BestShot.ubChanceToReallyHit) / 100)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("set ubAimTime = 0 for OCTH suppression"), gLogDecideActionRed); + BestShot.ubAimTime = 0; + } + + // reserve APs to hide if no cover or enemy is close + if (!AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || PythSpacesAway(pSoldier->sGridNo, BestShot.sTarget) < TACTICAL_RANGE / 2) + { + sReserveAP = APBPConstants[AP_MINIMUM] / 2; + } + if (PythSpacesAway(pSoldier->sGridNo, BestShot.sTarget) > TACTICAL_RANGE || AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || pSoldier->aiData.bUnderFire) + { + ubMinAuto *= 2; + } + + sActualAimAP = CalcAPCostForAiming(pSoldier, BestShot.sTarget, (INT8)pSoldier->aiData.bAimTime); + + if (UsingNewCTHSystem() == true) + { + do + { + pSoldier->bDoAutofire++; + dTotalRecoil += AICalcRecoilForShot(pSoldier, &(pSoldier->inv[BestShot.bWeaponIn]), pSoldier->bDoAutofire); + ubBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &(pSoldier->inv[BestShot.bWeaponIn]), pSoldier->bDoAutofire, pSoldier); + } while (pSoldier->bActionPoints >= BestShot.ubAPCost + sActualAimAP + ubBurstAPs + sReserveAP && + pSoldier->inv[pSoldier->ubAttackingHand][0]->data.gun.ubGunShotsLeft >= pSoldier->bDoAutofire && + pSoldier->bDoAutofire <= 30 && + (dTotalRecoil <= 20.0f || pSoldier->bDoAutofire < ubMinAuto)); + } + else + { + ubAutoPenalty = GetAutoPenalty(&pSoldier->inv[pSoldier->ubAttackingHand], gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_PRONE); + do + { + pSoldier->bDoAutofire++; + ubBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &(pSoldier->inv[BestShot.bWeaponIn]), pSoldier->bDoAutofire, pSoldier); + } while (pSoldier->bActionPoints >= BestShot.ubAPCost + sActualAimAP + ubBurstAPs + sReserveAP && + pSoldier->inv[pSoldier->ubAttackingHand][0]->data.gun.ubGunShotsLeft >= pSoldier->bDoAutofire && + pSoldier->bDoAutofire <= 30 && + (ubAutoPenalty * pSoldier->bDoAutofire <= 80 || pSoldier->bDoAutofire < ubMinAuto)); + } + + pSoldier->bDoAutofire--; + + // Make sure we decided to fire at least one shot! + ubBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &(pSoldier->inv[BestShot.bWeaponIn]), pSoldier->bDoAutofire, pSoldier); + DebugAI(AI_MSG_INFO, pSoldier, String("autofire shots %d APcost %d burst AP %d aimtime %d reserve AP %d", pSoldier->bDoAutofire, BestShot.ubAPCost, ubBurstAPs, sActualAimAP, sReserveAP), gLogDecideActionRed); + + // minimum 3 bullets + if (pSoldier->bDoAutofire >= 3 && pSoldier->bActionPoints >= BestShot.ubAPCost + sActualAimAP + ubBurstAPs + sReserveAP) + { + if (gAnimControl[pSoldier->usAnimState].ubEndHeight != BestShot.ubStance && + IsValidStance(pSoldier, BestShot.ubStance)) + { + pSoldier->aiData.bNextAction = AI_ACTION_FIRE_GUN; + pSoldier->aiData.usNextActionData = BestShot.sTarget; + pSoldier->aiData.bNextTargetLevel = BestShot.bTargetLevel; + pSoldier->aiData.usActionData = BestShot.ubStance; + + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before shooting"), gLogDecideActionRed); + + // show "suppression fire" message only if opponent cannot be seen after turning + if (!LOS_Raised(pSoldier, BestShot.ubOpponent, CALC_FROM_ALL_DIRS)) + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, New113Message[MSG113_SUPPRESSIONFIRE]); + + return(AI_ACTION_CHANGE_STANCE); + } + else + { + pSoldier->aiData.usActionData = BestShot.sTarget; + + // show "suppression fire" message only if opponent cannot be seen after turning + if (!LOS_Raised(pSoldier, BestShot.ubOpponent, CALC_FROM_ALL_DIRS)) + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, New113Message[MSG113_SUPPRESSIONFIRE]); + + DebugAI(AI_MSG_INFO, pSoldier, String("Suppression fire!"), gLogDecideActionRed); + return(AI_ACTION_FIRE_GUN); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Suppression not possible"), gLogDecideActionRed); + pSoldier->bDoBurst = 0; + pSoldier->bDoAutofire = 0; + } + } + } + // suppression not possible, do something else + + + //////////////////////////////////////////////////////////////////////// + // RADIO OPERATOR + //////////////////////////////////////////////////////////////////////// + decision = DecideActionRadioOperator(pSoldier, gLogDecideActionRed); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + } + + if (gGameExternalOptions.bNewTacticalAIBehavior) + { + //////////////////////////////////////////////////////////////////////////// + // IF YOU SEE CAPTURED FRIENDS, FREE THEM! + //////////////////////////////////////////////////////////////////////////// + + // Flugente: if we see one of our buddies captured, it is a clear sign of enemy activity! + if (gGameExternalOptions.fAllowPrisonerSystem && pSoldier->bTeam == ENEMY_TEAM) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Free friendly POWs]"), gLogDecideActionRed); + SoldierID ubPerson = GetClosestFlaggedSoldierID(pSoldier, 20, ENEMY_TEAM, SOLDIER_POW, TRUE); + + if (ubPerson != NOBODY) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Found friendly POW"), gLogDecideActionRed); + + // if we are close, we can release this guy + // possible only if not handcuffed (binders can be opened, handcuffs not) + if (!HasItemFlag(ubPerson->inv[HANDPOS].usItem, HANDCUFFS)) + { + if (PythSpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) < 2) + { + DebugAI(AI_MSG_INFO, pSoldier, String("I am close enough to free POW"), gLogDecideActionRed); + + // see if we are facing this person + UINT8 ubDesiredMercDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, ubPerson->sGridNo); + + // if not already facing in that direction, + if (pSoldier->ubDirection != ubDesiredMercDir) + { + pSoldier->aiData.usActionData = ubDesiredMercDir; + + DebugAI(AI_MSG_INFO, pSoldier, String("Change facing"), gLogDecideActionRed); + return(AI_ACTION_CHANGE_FACING); + } + + DebugAI(AI_MSG_INFO, pSoldier, String("Free POW"), gLogDecideActionRed); + return(AI_ACTION_FREE_PRISONER); + } + else + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Move closer to POW"), gLogDecideActionRed); + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // PROVIDE / SEEK MEDICAL AID + //////////////////////////////////////////////////////////////////////////// + + // if we are a doctor with medical gear, we might be able to help a wounded ally + if (pSoldier->CanMedicAI()) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Provide medical aid]"), gLogDecideActionRed); + + SoldierID ubPerson = GetClosestWoundedSoldierID(pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius, pSoldier->bTeam); + + // are we ourselves the patient? + if (ubPerson == pSoldier->ubID) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Patch ourselves up!"), gLogDecideActionRed); + + // if not already crouched, crouch down first + if (gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_CROUCH && IsValidStance(pSoldier, ANIM_CROUCH) && GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints) + { + pSoldier->aiData.usActionData = ANIM_CROUCH; + + DebugAI(AI_MSG_INFO, pSoldier, String("Crouch down"), gLogDecideActionRed); + return(AI_ACTION_CHANGE_STANCE); + } + + return(AI_ACTION_DOCTOR_SELF); + } + else if (ubPerson != NOBODY) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Someone else is injured"), gLogDecideActionRed); + + if (PythSpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) < 2) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Wounded soldier is nearby"), gLogDecideActionRed); + + // see if we are facing this person + UINT8 ubDesiredMercDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, ubPerson->sGridNo); + + // if not already facing in that direction, + if (pSoldier->ubDirection != ubDesiredMercDir) + { + pSoldier->aiData.usActionData = ubDesiredMercDir; + + DebugAI(AI_MSG_INFO, pSoldier, String("Change facing"), gLogDecideActionRed); + return(AI_ACTION_CHANGE_FACING); + } + + // if not already crouched, crouch down first + if (gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_CROUCH && IsValidStance(pSoldier, ANIM_CROUCH) && GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints) + { + pSoldier->aiData.usActionData = ANIM_CROUCH; + + DebugAI(AI_MSG_INFO, pSoldier, String("Crouch down"), gLogDecideActionRed); + return(AI_ACTION_CHANGE_STANCE); + } + + DebugAI(AI_MSG_INFO, pSoldier, String("Administer aid"), gLogDecideActionRed); + return(AI_ACTION_DOCTOR); + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Wounded soldier is far"), gLogDecideActionRed); + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Try to move towards the wounded person"), gLogDecideActionRed); + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + // if we are not a medic, but are wounded, seek a medic + else if (pSoldier->iHealableInjury >= gGameExternalOptions.sEnemyMedicsWoundMinAmount) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Seek medical aid]"), gLogDecideActionRed); + + SoldierID ubPerson = GetClosestMedicSoldierID(pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius / 2, pSoldier->bTeam); + + if (ubPerson != NOBODY) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Found a medic!"), gLogDecideActionRed); + + if (PythSpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) > 1) + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek aid"), gLogDecideActionRed); + return(AI_ACTION_SEEK_FRIEND); + } + } + } + else { DebugAI(AI_MSG_INFO, pSoldier, String("No medics around! :("), gLogDecideActionRed); } + } + + + //////////////////////////////////////////////////////////////////////////// + // VIP RETREAT + //////////////////////////////////////////////////////////////////////////// + decision = DecideActionVIPretreat(pSoldier, gLogDecideActionRed); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + + //////////////////////////////////////////////////////////////////////////// + // PROTECT VIP + //////////////////////////////////////////////////////////////////////////// + // are we a bodyguard? + if (pSoldier->usSoldierFlagMask & SOLDIER_BODYGUARD) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Bodyguard]"), gLogDecideActionRed); + // is VIP still alive? + SoldierID ubPerson = GetClosestFlaggedSoldierID(pSoldier, 100, pSoldier->bTeam, SOLDIER_VIP, FALSE); + + if (ubPerson != NOBODY) + { + DebugAI(AI_MSG_INFO, pSoldier, String("VIP found"), gLogDecideActionRed); + // we want to stay close to him, but still be able to function properly... stay withing a 7-tile radius + if (SpacesAway(pSoldier->sGridNo, ubPerson->sGridNo) > 7) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Attempt to get close "), gLogDecideActionRed); + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, ubPerson->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek VIP"), gLogDecideActionRed); + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } + } + + //////////////////////////////////////////////////////////////////////// + // RED RETREAT + //////////////////////////////////////////////////////////////////////// + if (gfTurnBasedAI && + !bInWater && + ubCanMove && + pSoldier->aiData.bOrders != STATIONARY && + pSoldier->aiData.bOrders != SNIPER && + pSoldier->RetreatCounterValue() > 0 && + (pSoldier->CheckInitialAP() || !fAnyCover || pSoldier->aiData.bUnderFire)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[retreat]"), gLogDecideActionRed); + DebugAI(AI_MSG_TOPIC, pSoldier, String("search for retreat spot"), gLogDecideActionRed); + INT32 sRetreatSpot = FindRetreatSpot(pSoldier); + + if (!TileIsOutOfBounds(sRetreatSpot)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("found retreat spot %d", sRetreatSpot), gLogDecideActionRed); + + //BeginMultiPurposeLocator(sRetreatSpot, pSoldier->pathing.bLevel, FALSE); + + pSoldier->aiData.usActionData = sRetreatSpot; + return(AI_ACTION_TAKE_COVER); + } + } + + + //////////////////////////////////////////////////////////////////////// + // CROUCH & REST IF RUNNING OUT OF BREATH + //////////////////////////////////////////////////////////////////////// + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: crouch and rest if running out of breath"); + + // if our breath is running a bit low, and we're not in water or under fire + if ((pSoldier->bBreath < 25) && !bInWater && !pSoldier->aiData.bUnderFire) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Running out of breath, try to rest]"), gLogDecideActionRed); + // if not already crouched, try to crouch down first + if (!PTR_CROUCHED && IsValidStance(pSoldier, ANIM_CROUCH) && gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_PRONE) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s CROUCHES, NEEDING REST (STATUS RED), breath = %d", pSoldier->name, pSoldier->bBreath); + AIPopMessage(tempstr); +#endif + + if (!gfTurnBasedAI || GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints) + { + pSoldier->aiData.usActionData = ANIM_CROUCH; + + DebugAI(AI_MSG_INFO, pSoldier, String("Crouch"), gLogDecideActionRed); + return(AI_ACTION_CHANGE_STANCE); + } + } + +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s RESTS (STATUS RED), breath = %d", pSoldier->name, pSoldier->bBreath); + AIPopMessage(tempstr); +#endif + + pSoldier->aiData.usActionData = NOWHERE; + + // is it a heavy gun? And we have energy cost for shooting enabled? + if (WeaponReady(pSoldier) && GetBPCostPer10APsForGunHolding(pSoldier) > 0) + { + // unready + DebugAI(AI_MSG_INFO, pSoldier, String("Lower weapon"), gLogDecideActionRed); + return(AI_ACTION_LOWER_GUN); + } + + DebugAI(AI_MSG_INFO, pSoldier, String("Rest"), gLogDecideActionRed); + return(AI_ACTION_NONE); + } + + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: calculate morale"); + // calculate our morale + pSoldier->aiData.bAIMorale = CalcMorale(pSoldier); + // WDS DEBUG - this will make all enemies run away (to test retreating into occupied sector bugs) + // pSoldier->aiData.bAIMorale = MORALE_HOPELESS; + + // if a guy is feeling REALLY discouraged, he may continue to run like hell + if ((pSoldier->aiData.bAIMorale == MORALE_HOPELESS) && ubCanMove) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Low morale, attempting to run away]"), gLogDecideActionRed); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: run away"); + //////////////////////////////////////////////////////////////////////// + // RUN AWAY TO SPOT FARTHEST FROM KNOWN THREATS (ONLY IF MORALE HOPELESS) + //////////////////////////////////////////////////////////////////////// + + // look for best place to RUN AWAY to (farthest from the closest threat) + pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s RUNNING AWAY to grid %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + DebugAI(AI_MSG_INFO, pSoldier, String("Running away to grid %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + return(AI_ACTION_RUN_AWAY); + } + } + + + //////////////////////////////////////////////////////////////////////////// + // RADIO RED ALERT: determine %chance to call others and report contact + //////////////////////////////////////////////////////////////////////////// + if (!bInDeepWater) + { + auto decision = DecideActionRadioRedAlert(pSoldier, gLogDecideActionRed); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + } + + + //////////////////////////////////////////////////////////////////////////// + // THROW A SMOKE GRENADE FOR COVER + //////////////////////////////////////////////////////////////////////////// + if (gfTurnBasedAI && + pSoldier->bActionPoints == pSoldier->bInitialActionPoints && + pSoldier->aiData.bUnderFire && + !InARoom(pSoldier->sGridNo, NULL) && + !InSmoke(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + RangeChangeDesire(pSoldier) <= 2 && + (!NightLight() || InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel)) && + !TileIsOutOfBounds(sClosestOpponent) && + distanceToOpponent > 10*TACTICAL_RANGE / 4 && + (!fProneSightCover && !AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || pSoldier->TakenLargeHit()) && + (pSoldier->TakenLargeHit() || pSoldier->ShockLevelPercent() > 20 + Random(80))) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Self smoke when under fire]"), gLogDecideActionRed); + CheckTossSelfSmoke(pSoldier, &BestThrow); + + if (BestThrow.ubPossible) + { + DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime), gLogDecideActionRed); + + // start retreating for several turns + pSoldier->RetreatCounterStart(2); + + // if necessary, swap the usItem from holster into the hand position + if (BestThrow.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket"), gLogDecideActionRed); + RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); + } + + // stand up before throwing if needed + if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && + pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before throw"), gLogDecideActionRed); + pSoldier->aiData.usActionData = BestThrow.ubStance; + pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; + pSoldier->aiData.usNextActionData = BestThrow.sTarget; + pSoldier->aiData.bNextTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + return AI_ACTION_CHANGE_STANCE; + } + else + { + pSoldier->aiData.usActionData = BestThrow.sTarget; + pSoldier->bTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + } + + DebugAI(AI_MSG_INFO, pSoldier, String("Throw smoke!"), gLogDecideActionRed); + return(AI_ACTION_TOSS_PROJECTILE); + } + else { DebugAI(AI_MSG_INFO, pSoldier, String("Throw not possible"), gLogDecideActionRed); } + } + + if (!(pSoldier->flags.uiStatusFlags & (SOLDIER_DRIVER | SOLDIER_PASSENGER))) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: main red ai"); + + + //////////////////////////////////////////////////////////////////////////// + // AVOID LIGHT IF SPOT IS DANGEROUS AND NO FRIENDS SEE MY CLOSEST ENEMY + //////////////////////////////////////////////////////////////////////////// + if (ubCanMove && + InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + pSoldier->aiData.bOrders != STATIONARY && + pSoldier->aiData.bOrders != SNIPER && + CountFriendsBlack(pSoldier) == 0) + { + pSoldier->aiData.usActionData = FindNearbyDarkerSpot(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + // move as if leaving water or gas + DebugAI(AI_MSG_INFO, pSoldier, String("Move out of light"), gLogDecideActionRed); + return(AI_ACTION_LEAVE_WATER_GAS); + } + } + + //////////////////////////////////////////////////////////////////////////// + // MAIN RED AI: Decide soldier's preference between SEEKING,HELPING & HIDING + //////////////////////////////////////////////////////////////////////////// + + // get the location of the closest reachable opponent + sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb); + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: check to continue flanking"); + // continue flanking + INT32 sFlankGridNo; + + if (TileIsOutOfBounds(sClosestDisturbance)) + sFlankGridNo = pSoldier->lastFlankSpot; + else + sFlankGridNo = sClosestDisturbance; + + // continue flanking + // sevenfm: dont' flank when under fire + if (pSoldier->numFlanks > 0 && + pSoldier->numFlanks < MAX_FLANKS_RED && + gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_PRONE && + !pSoldier->aiData.bUnderFire) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Continue flanking]"), gLogDecideActionRed); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: continue flanking"); + INT16 currDir = GetDirectionFromGridNo(sFlankGridNo, pSoldier); + INT16 origDir = pSoldier->origDir; + pSoldier->numFlanks += 1; + if (pSoldier->flags.lastFlankLeft) + { + if (origDir > currDir) + origDir -= NUM_WORLD_DIRECTIONS; + + // stop flanking condition + if ((currDir - origDir) >= MinFlankDirections(pSoldier)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking, left"), gLogDecideActionRed); + pSoldier->numFlanks = MAX_FLANKS_RED; + } + else + { + pSoldier->aiData.usActionData = FindFlankingSpot(pSoldier, sFlankGridNo, AI_ACTION_FLANK_LEFT); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) //&& (currDir - origDir) < 2 ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Flank left"), gLogDecideActionRed); + return AI_ACTION_FLANK_LEFT; + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking left, tile out of bounds"), gLogDecideActionRed); + pSoldier->numFlanks = MAX_FLANKS_RED; + } + } + } + else + { + if (origDir < currDir) + origDir += NUM_WORLD_DIRECTIONS; + + // stop flanking condition + if ((origDir - currDir) >= MinFlankDirections(pSoldier)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking, right"), gLogDecideActionRed); + pSoldier->numFlanks = MAX_FLANKS_RED; + } + else + { + pSoldier->aiData.usActionData = FindFlankingSpot(pSoldier, sFlankGridNo, AI_ACTION_FLANK_RIGHT); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))//&& (origDir - currDir) < 2 ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Flank right"), gLogDecideActionRed); + return AI_ACTION_FLANK_RIGHT; + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking right, tile ouf of bounds"), gLogDecideActionRed); + pSoldier->numFlanks = MAX_FLANKS_RED; + } + } + } + } + + // 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"); + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Stop flanking]"), gLogDecideActionRed); + + // start end flank approach with full APs + if (gfTurnBasedAI && pSoldier->bActionPoints < pSoldier->bInitialActionPoints) + { + DebugAI(AI_MSG_INFO, pSoldier, String("AP not full, wait a turn"), gLogDecideActionRed); + return(AI_ACTION_END_TURN); + } + + if (!TileIsOutOfBounds(sFlankGridNo) && + !GuySawEnemy(pSoldier) && + !pSoldier->aiData.bUnderFire && + !Water(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + pSoldier->bInitialActionPoints >= APBPConstants[AP_MINIMUM] && + (PythSpacesAway(pSoldier->sGridNo, sFlankGridNo) > MIN_FLANK_DIST_RED || + !LocationToLocationLineOfSightTest(pSoldier->sGridNo, pSoldier->pathing.bLevel, sFlankGridNo, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS))) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Move towards enemy"), gLogDecideActionRed); + + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sFlankGridNo, 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, pSoldier->pathing.bLevel) && + !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, sFlankGridNo, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS) && + !LocationToLocationLineOfSightTest(pSoldier->sGridNo, pSoldier->pathing.bLevel, sFlankGridNo, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Can be seen in new position, prepare crouch & shot"), gLogDecideActionRed); + + // reserve APs for a possible crouch plus a shot + INT32 sCautiousGridNo = InternalGoAsFarAsPossibleTowards(pSoldier, sFlankGridNo, (INT8)(MinAPsToAttack(pSoldier, sFlankGridNo, ADDTURNCOST, 0) + GetAPsCrouch(pSoldier, TRUE) + GetAPsToLook(pSoldier)), AI_ACTION_SEEK_OPPONENT, FLAG_CAUTIOUS); + + if (!TileIsOutOfBounds(sCautiousGridNo)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy to cautiosgridno %d", sCautiousGridNo), gLogDecideActionRed); + pSoldier->aiData.usActionData = sCautiousGridNo; + pSoldier->aiData.fAIFlags |= AI_CAUTIOUS; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_SEEK_OPPONENT); + } + + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy to gridno %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + return(AI_ACTION_SEEK_OPPONENT); + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy"), gLogDecideActionRed); + return(AI_ACTION_SEEK_OPPONENT); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Can't advance, stop flanking"), gLogDecideActionRed); + // if we cannot advance to spot, stop trying + pSoldier->numFlanks++; + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Stop flanking"), gLogDecideActionRed); + // stop + pSoldier->numFlanks++; + } + } + + if (pSoldier->CheckInitialAP() && + pSoldier->bActionPoints >= APBPConstants[AP_MINIMUM] && + gfTurnBasedAI && + pSoldier->pathing.bLevel == 0 && + !pSoldier->aiData.bUnderFire && + !InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + SightCoverAtSpot(pSoldier, pSoldier->sGridNo, TRUE) && + !GuySawEnemy(pSoldier) && + !TileIsOutOfBounds(sClosestDisturbance) && + //!fSeekClimb && + PythSpacesAway(pSoldier->sGridNo, sClosestDisturbance) < TACTICAL_RANGE && + (pSoldier->aiData.bOrders == STATIONARY || pSoldier->aiData.bOrders == SNIPER || RangeChangeDesire(pSoldier) < 4) && + !SoldierToVirtualSoldierLineOfSightTest(pSoldier, sClosestDisturbance, pSoldier->pathing.bLevel, ANIM_STAND, TRUE, CALC_FROM_ALL_DIRS) && + CountFriendsBlack(pSoldier, sClosestDisturbance) == 0) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Set watched location]"), gLogDecideActionRed); + gubNPCAPBudget = 0; + gubNPCDistLimit = 0; + + // check path to closest disturbance and find the point where enemy will appear in sight + if (FindBestPath(pSoldier, sClosestDisturbance, pSoldier->pathing.bLevel, RUNNING, COPYROUTE, PATH_IGNORE_PERSON_AT_DEST | PATH_THROUGH_PEOPLE)) + { + INT16 sLoop; + INT32 sLastSeenSpot = NOWHERE; + + DebugAI(AI_MSG_INFO, pSoldier, String("found path to %d, path size %d ", sClosestDisturbance, pSoldier->pathing.usPathDataSize), gLogDecideActionRed); + DebugAI(AI_MSG_INFO, pSoldier, String("check path for seen spots"), gLogDecideActionRed); + + sCheckGridNo = pSoldier->sGridNo; + + for (sLoop = pSoldier->pathing.usPathIndex; sLoop < pSoldier->pathing.usPathDataSize; sLoop++) + { + sCheckGridNo = NewGridNo(sCheckGridNo, DirectionInc((UINT8)(pSoldier->pathing.usPathingData[sLoop]))); + + if (SoldierToVirtualSoldierLineOfSightTest(pSoldier, sCheckGridNo, pSoldier->pathing.bLevel, ANIM_STAND, TRUE, CALC_FROM_ALL_DIRS)) + { + sLastSeenSpot = sCheckGridNo; + } + } + + // if found last seen spot + if (!TileIsOutOfBounds(sLastSeenSpot)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("last seen spot %d level %d", sLastSeenSpot, pSoldier->pathing.bLevel), gLogDecideActionRed); + IncrementWatchedLoc(pSoldier->ubID, sLastSeenSpot, pSoldier->pathing.bLevel); + } + } + gubNPCAPBudget = 0; + } + + // if we can move at least 1 square's worth + // and have more APs than we want to reserve + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: can we move? = %d, APs = %d", ubCanMove, pSoldier->bActionPoints)); + + if (ubCanMove && pSoldier->bActionPoints > APBPConstants[MAX_AP_CARRIED]) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: checking hide/seek/help/watch points... orders = %d, attitude = %d", pSoldier->aiData.bOrders, pSoldier->aiData.bAttitude)); + DebugAI(AI_MSG_INFO, pSoldier, String("checking hide/seek/help/watch points... orders = %d, attitude = %d", pSoldier->aiData.bOrders, pSoldier->aiData.bAttitude), gLogDecideActionRed); + // calculate initial points for watch based on highest watch loc + + bWatchPts = GetHighestWatchedLocPoints(pSoldier->ubID); + if (bWatchPts <= 0) + { + // no watching + bWatchPts = -99; + } + + // modify RED movement tendencies according to morale + switch (pSoldier->aiData.bAIMorale) + { + case MORALE_HOPELESS: bSeekPts = -99; bHelpPts = -99; bHidePts += +2; bWatchPts = -99; break; + case MORALE_WORRIED: bSeekPts += -2; bHelpPts += 0; bHidePts += +2; bWatchPts += 1; break; + case MORALE_NORMAL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case MORALE_CONFIDENT: bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += 0; break; + case MORALE_FEARLESS: bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += 0; break; + } + + // modify tendencies according to orders + switch (pSoldier->aiData.bOrders) + { + case STATIONARY: bSeekPts += -1; bHelpPts += -1; bHidePts += +1; bWatchPts += +1; break; + case ONGUARD: bSeekPts += -1; bHelpPts += 0; bHidePts += +1; bWatchPts += +1; break; + case CLOSEPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case RNDPTPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case POINTPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case FARPATROL: bSeekPts += 0; bHelpPts += 0; bHidePts += 0; bWatchPts += 0; break; + case ONCALL: bSeekPts += 0; bHelpPts += +1; bHidePts += -1; bWatchPts += 0; break; + case SEEKENEMY: bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += -1; break; + case SNIPER: bSeekPts += -1; bHelpPts += 0; bHidePts += +1; bWatchPts += +1; break; + } + + // modify tendencies according to attitude + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: bSeekPts += -1; bHelpPts += 0; bHidePts += +2; bWatchPts += +1; break; + case BRAVESOLO: bSeekPts += +1; bHelpPts += -1; bHidePts += -1; bWatchPts += -1; break; + case BRAVEAID: bSeekPts += +1; bHelpPts += +1; bHidePts += -1; bWatchPts += -1; break; + case CUNNINGSOLO: bSeekPts += 1; bHelpPts += -1; bHidePts += +1; bWatchPts += 0; break; + case CUNNINGAID: bSeekPts += 1; bHelpPts += +1; bHidePts += +1; bWatchPts += 0; break; + case AGGRESSIVE: bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += 0; break; + case ATTACKSLAYONLY:bSeekPts += +1; bHelpPts += 0; bHidePts += -1; bWatchPts += 0; break; + } + + // sevenfm: snipers and soldiers with scoped guns should decide watch more often + if (AIGunScoped(pSoldier) || AICheckIsSniper(pSoldier)) + { + bWatchPts++; + } + + // sevenfm: disable watching if soldier is under fire or in dangerous place + // don't watch if some friends can see my closest opponent + if (fDangerousSpot || + InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel) || + CountFriendsBlack(pSoldier) > 0) + { + // prefer hiding when in dangerous place + if (bHidePts > -90) + bWatchPts = min(bWatchPts, bHidePts - 1); + else + bWatchPts--; + } + + // sevenfm: don't watch when overcrowded and not in a building + if (!InARoom(pSoldier->sGridNo, NULL)) + { + bWatchPts -= CountNearbyFriends(pSoldier, pSoldier->sGridNo, TACTICAL_RANGE / 8); + } + + // sevenfm: don't help if seen enemy recently or under fire + if (GuySawEnemy(pSoldier) || pSoldier->aiData.bUnderFire) + { + bHelpPts -= 10; + } + + if (pSoldier->RetreatCounterValue() > 0) + { + // no seeking when retreating + bSeekPts = -99; + // no helping when retreating + bHelpPts = -99; + + if (bHidePts > -90) + { + bWatchPts = min(bWatchPts, bHidePts - 1); + } + } + + if (!gfTurnBasedAI) + { + // don't search for cover + bHidePts = -99; + } + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("decideactionred: hide = %d, seek = %d, watch = %d, help = %d", bHidePts, bSeekPts, bWatchPts, bHelpPts)); + DebugAI(AI_MSG_INFO, pSoldier, String("hide = %d, seek = %d, watch = %d, help = %d", bHidePts, bSeekPts, bWatchPts, bHelpPts), gLogDecideActionRed); + // while one of the three main RED REACTIONS remains viable + while ((bSeekPts > -90) || (bHelpPts > -90) || (bHidePts > -90)) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: checking to seek"); + // if SEEKING is possible and at least as desirable as helping or hiding + if (((bSeekPts > -90) && (bSeekPts >= bHelpPts) && (bSeekPts >= bHidePts) && (bSeekPts >= bWatchPts))) + { +#ifdef AI_TIMING_TESTS + uiStartTime = GetJA2Clock(); +#endif + +#ifdef AI_TIMING_TESTS + uiEndTime = GetJA2Clock(); + guiRedSeekTimeTotal += (uiEndTime - uiStartTime); + guiRedSeekCounter++; +#endif + // if there is an opponent reachable + // sevenfm: allow seeking in prone stance if we haven't seen enemy for several turns + if (!TileIsOutOfBounds(sClosestDisturbance) && + (gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_PRONE || !GuySawEnemy(pSoldier))) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: seek opponent"); + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy"), gLogDecideActionRed); + ////////////////////////////////////////////////////////////////////// + // SEEK CLOSEST DISTURBANCE: GO DIRECTLY TOWARDS CLOSEST KNOWN OPPONENT + ////////////////////////////////////////////////////////////////////// + + // try to move towards him + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestDisturbance, GetAPsCrouch(pSoldier, TRUE), AI_ACTION_SEEK_OPPONENT, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + // Check for a trap + if (!ArmySeesOpponents()) + { + if (GetNearestRottingCorpseAIWarning(pSoldier->aiData.usActionData) > 0) + { + // abort! abort! + pSoldier->aiData.usActionData = NOWHERE; + } + } + } + + // if it's possible + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + // do it! + sprintf(tempstr, "%s - SEEKING OPPONENT at grid %d, MOVING to %d", + pSoldier->name, sClosestDisturbance, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + if (fClimb)//&& pSoldier->aiData.usActionData == sClosestDisturbance) + { + // need to climb AND have enough APs to get there this turn + BOOLEAN fUp = TRUE; + if (pSoldier->pathing.bLevel > 0) + fUp = FALSE; + + if (!fUp) + DebugMsg(TOPIC_JA2AI, DBG_LEVEL_3, String("Soldier %d is climbing down", pSoldier->ubID)); + + // As mentioned in the next part, the sClosestDisturbance IS the climb point desired. So the + // check here should be "Am I aready there?" If so, THEN possibly climb. This previous check + // would have a soldier climbing any building, even if it was not the desired building. So + // WRONG WRONG WRONG + //if ( CanClimbFromHere ( pSoldier, fUp ) ) + if (pSoldier->sGridNo == sClosestDisturbance) + { + if (IsActionAffordable(pSoldier) && pSoldier->bActionPoints >= (APBPConstants[AP_CLIMBROOF] + MinAPsToAttack(pSoldier, sClosestDisturbance, ADDTURNCOST, 0))) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Climb roof at gridno %d", sClosestDisturbance), gLogDecideActionRed); + return(AI_ACTION_CLIMB_ROOF); + } + } + else + { + // Do not overwrite the usActionData here. If there's no nearby climb point, the action data + // would become NOWHERE, and then the SEEK_ENEMY fallback would also fail. + // In fact, sClosestDisturbance has ALREADY calculated the closest climb point when climbing is + // necessary. The returned grid # in sClosestDisturbance is that climb point. So if climb is + // set, then use sClosestDisturbance as is. + //INT16 usClimbPoint = FindClosestClimbPoint(pSoldier, pSoldier->sGridNo , sClosestDisturbance , fUp ); + INT32 usClimbPoint = sClosestDisturbance; + if (!TileIsOutOfBounds(usClimbPoint)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Move towards climb spot %d", usClimbPoint), gLogDecideActionRed); + pSoldier->aiData.usActionData = usClimbPoint; + return(AI_ACTION_MOVE_TO_CLIMB); + } + } + } + //if ( fClimb && pSoldier->aiData.usActionData == sClosestDisturbance) + //{ + // return( AI_ACTION_CLIMB_ROOF ); + //} + + BOOLEAN fOvercrowded = FALSE; + if (CountNearbyFriends(pSoldier, pSoldier->sGridNo, TACTICAL_RANGE / 4) > 2) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Soldier position %d is overcrowded", pSoldier->sGridNo), gLogDecideActionRed); + fOvercrowded = TRUE; + } + + // sevenfm: possibly start RED flanking + if ((pSoldier->aiData.bAttitude == CUNNINGAID || pSoldier->aiData.bAttitude == CUNNINGSOLO || + (pSoldier->aiData.bAttitude == BRAVESOLO || pSoldier->aiData.bAttitude == BRAVEAID) && fOvercrowded) && + pSoldier->bTeam == ENEMY_TEAM && + gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_PRONE && + !pSoldier->aiData.bUnderFire && + pSoldier->pathing.bLevel == 0 && + (pSoldier->aiData.bOrders == SEEKENEMY || + pSoldier->aiData.bOrders == FARPATROL || + pSoldier->aiData.bOrders == CLOSEPATROL && NightTime()) && + (!GuySawEnemy(pSoldier) || fOvercrowded) && + !Water(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + pSoldier->bActionPoints >= APBPConstants[AP_MINIMUM] && + (CountFriendsInDirection(pSoldier, sClosestDisturbance) > 1 || NightTime() || fOvercrowded)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Possibly start flanking]"), gLogDecideActionRed); + INT8 action = AI_ACTION_SEEK_OPPONENT; + INT16 dist = PythSpacesAway(pSoldier->sGridNo, sClosestDisturbance); + if (dist > MIN_FLANK_DIST_RED && dist < MAX_FLANK_DIST_RED) + { + INT16 rdm = Random(6); + + switch (rdm) + { + case 1: + case 2: + case 3: + if (pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Try to flank left"), gLogDecideActionRed); + action = AI_ACTION_FLANK_LEFT; + } + break; + default: + if (pSoldier->aiData.bLastAction != AI_ACTION_FLANK_LEFT && pSoldier->aiData.bLastAction != AI_ACTION_FLANK_RIGHT) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Try to flank right"), gLogDecideActionRed); + action = AI_ACTION_FLANK_RIGHT; + } + break; + } + + if (action == AI_ACTION_SEEK_OPPONENT) { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy instead"), gLogDecideActionRed); + return action; + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Distance not suitable, seek enemy instead"), gLogDecideActionRed); + return AI_ACTION_SEEK_OPPONENT; + } + pSoldier->aiData.usActionData = FindFlankingSpot(pSoldier, sClosestDisturbance, action); + + if (TileIsOutOfBounds(pSoldier->aiData.usActionData) || pSoldier->numFlanks >= MAX_FLANKS_RED) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Flanking spot %d out of bounds or numFlanks >= MAX_FLANKS_RED", pSoldier->aiData.usActionData), gLogDecideActionRed); + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestDisturbance, GetAPsCrouch(pSoldier, TRUE), AI_ACTION_SEEK_OPPONENT, 0); + //pSoldier->numFlanks = 0; + if (PythSpacesAway(pSoldier->aiData.usActionData, sClosestDisturbance) < 5 || LocationToLocationLineOfSightTest(pSoldier->aiData.usActionData, pSoldier->pathing.bLevel, sClosestDisturbance, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS)) + { + // reserve APs for a possible crouch plus a shot + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestDisturbance, (INT8)(MinAPsToAttack(pSoldier, sClosestDisturbance, ADDTURNCOST, 0) + GetAPsCrouch(pSoldier, TRUE)), AI_ACTION_SEEK_OPPONENT, FLAG_CAUTIOUS); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Reserved AP for crouch & shot, seek enemy"), gLogDecideActionRed); + pSoldier->aiData.fAIFlags |= AI_CAUTIOUS; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_SEEK_OPPONENT); + } + } + + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy"), gLogDecideActionRed); + return(AI_ACTION_SEEK_OPPONENT); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Found flanking spot %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + if (action == AI_ACTION_FLANK_LEFT) + pSoldier->flags.lastFlankLeft = TRUE; + else + pSoldier->flags.lastFlankLeft = FALSE; + + if (pSoldier->lastFlankSpot != sClosestDisturbance) + pSoldier->numFlanks = 0; + + + pSoldier->origDir = GetDirectionFromGridNo(sClosestDisturbance, pSoldier); + pSoldier->lastFlankSpot = sClosestDisturbance; + pSoldier->numFlanks++; + + // sevenfm: change orders when starting to flank + if (pSoldier->aiData.bOrders == CLOSEPATROL) + { + pSoldier->aiData.bOrders = FARPATROL; + } + + DebugAI(AI_MSG_INFO, pSoldier, String("Start flanking"), gLogDecideActionRed); + return(action); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Not flanking, move up towards enemy"), gLogDecideActionRed); + // let's be a bit cautious about going right up to a location without enough APs to shoot + if (PythSpacesAway(pSoldier->aiData.usActionData, sClosestDisturbance) < 5 || LocationToLocationLineOfSightTest(pSoldier->aiData.usActionData, pSoldier->pathing.bLevel, sClosestDisturbance, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS)) + { + // reserve APs for a possible crouch plus a shot + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestDisturbance, (INT8)(MinAPsToAttack(pSoldier, sClosestDisturbance, ADDTURNCOST, 0) + GetAPsCrouch(pSoldier, TRUE)), AI_ACTION_SEEK_OPPONENT, FLAG_CAUTIOUS); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Reserved AP for crouch & shot, seek enemy"), gLogDecideActionRed); + pSoldier->aiData.fAIFlags |= AI_CAUTIOUS; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_SEEK_OPPONENT); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Seek enemy"), gLogDecideActionRed); + return(AI_ACTION_SEEK_OPPONENT); + } + break; + } + } + } + + // mark SEEKING as impossible for next time through while loop +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "couldn't SEEK...", 1000); +#endif + bSeekPts = -99; + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: couldn't seek"); + } + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: checking to watch"); + // if WATCHING is possible and at least as desirable as anything else + if ((bWatchPts > -90) && (bWatchPts >= bSeekPts) && (bWatchPts >= bHelpPts) && (bWatchPts >= bHidePts)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("[watch]"), gLogDecideActionRed); + // take a look at our highest watch point... if it's still visible, turn to face it and then wait + bHighestWatchLoc = GetHighestVisibleWatchedLoc(pSoldier->ubID); + + if (bHighestWatchLoc != -1) + { + // see if we need turn to face that location + ubOpponentDir = AIDirection(pSoldier->sGridNo, gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc]); + DebugAI(AI_MSG_INFO, pSoldier, String("Highest watch location: [%d] %d %d watch dir: %d", bHighestWatchLoc, gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc], gbWatchedLocLevel[pSoldier->ubID][bHighestWatchLoc], ubOpponentDir), gLogDecideActionRed); + + // consider at least crouching + if (gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_STAND && + IsValidStance(pSoldier, ANIM_CROUCH) && + pSoldier->bActionPoints >= GetAPsCrouch(pSoldier, TRUE)) + { + pSoldier->aiData.usActionData = ANIM_CROUCH; + + DebugAI(AI_MSG_INFO, pSoldier, String("crouch to watch"), gLogDecideActionRed); + return(AI_ACTION_CHANGE_STANCE); + } + + // raise weapon if not raised + if (PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION && + !WeaponReady(pSoldier) && + (pSoldier->bBreath > OKBREATH * 2 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 50) && + pSoldier->bActionPoints >= GetAPsToReadyWeapon(pSoldier, PickSoldierReadyAnimation(pSoldier, FALSE, FALSE))) + { + DebugAI(AI_MSG_INFO, pSoldier, String("raise weapon"), gLogDecideActionRed); + return AI_ACTION_RAISE_GUN; + } + + // if soldier is not already facing in that direction + if (pSoldier->ubDirection != ubOpponentDir && + pSoldier->InternalIsValidStance(ubOpponentDir, gAnimControl[pSoldier->usAnimState].ubEndHeight) && + pSoldier->bActionPoints >= GetAPsToLook(pSoldier)) + { + // turn + pSoldier->aiData.usActionData = ubOpponentDir; + DebugAI(AI_MSG_INFO, pSoldier, String("turn to watched location"), gLogDecideActionRed); + return(AI_ACTION_CHANGE_FACING); + } + + // possibly go prone, check that we'll have line of sight to standing enemy at watched location + if (gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_CROUCH && + IsValidStance(pSoldier, ANIM_PRONE) && + pSoldier->bActionPoints >= GetAPsProne(pSoldier, TRUE) && + (!InARoom(pSoldier->sGridNo, NULL) || pSoldier->pathing.bLevel > 0 || pSoldier->aiData.bUnderFire) && + gfTurnBasedAI && + LocationToLocationLineOfSightTest(pSoldier->sGridNo, pSoldier->pathing.bLevel, gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc], gbWatchedLocLevel[pSoldier->ubID][bHighestWatchLoc], TRUE, pSoldier->GetMaxDistanceVisible(gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc], gbWatchedLocLevel[pSoldier->ubID][bHighestWatchLoc], CALC_FROM_ALL_DIRS), PRONE_LOS_POS, STANDING_LOS_POS)) + { + pSoldier->aiData.usActionData = ANIM_PRONE; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + DebugAI(AI_MSG_INFO, pSoldier, String("go prone, end turn"), gLogDecideActionRed); + return(AI_ACTION_CHANGE_STANCE); + } + + DebugAI(AI_MSG_INFO, pSoldier, String("watch at %d level %d", gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc], gbWatchedLocLevel[pSoldier->ubID][bHighestWatchLoc]), gLogDecideActionRed); + return(AI_ACTION_NONE); + } + + bWatchPts = -99; + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: couldn't watch"); + } + + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: checking to help"); + // if HELPING is possible and at least as desirable as seeking or hiding + if ((bHelpPts > -90) && (bHelpPts >= bSeekPts) && (bHelpPts >= bHidePts) && (bHelpPts >= bWatchPts)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Help a friend]"), gLogDecideActionRed); +#ifdef AI_TIMING_TESTS + uiStartTime = GetJA2Clock(); +#endif + INT32 sClosestFriend = ClosestReachableFriendInTrouble(pSoldier, &fClimb); +#ifdef AI_TIMING_TESTS + uiEndTime = GetJA2Clock(); + + guiRedHelpTimeTotal += (uiEndTime - uiStartTime); + guiRedHelpCounter++; +#endif + //WarmSteel - Dont try if we're already quite close to our friend + // sevenfm: reverted to vanilla helping + //if (!TileIsOutOfBounds(sClosestFriend) && PythSpacesAway(pSoldier->sGridNo, sClosestFriend) > pSoldier->GetMaxDistanceVisible(sClosestFriend, 0, CALC_FROM_ALL_DIRS )) + if (!TileIsOutOfBounds(sClosestFriend)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Closest friend at gridno %d", sClosestFriend), gLogDecideActionRed); + ////////////////////////////////////////////////////////////////////// + // GO DIRECTLY TOWARDS CLOSEST FRIEND UNDER FIRE OR WHO LAST RADIOED + ////////////////////////////////////////////////////////////////////// + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestFriend, GetAPsCrouch(pSoldier, TRUE), AI_ACTION_SEEK_OPPONENT, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - SEEKING FRIEND at %d, MOVING to %d", + pSoldier->name, sClosestFriend, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + DebugAI(AI_MSG_INFO, pSoldier, String("Seeking friend, moving to %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + + if (fClimb)//&& pSoldier->aiData.usActionData == sClosestFriend) + { + // need to climb AND have enough APs to get there this turn + BOOLEAN fUp = TRUE; + if (pSoldier->pathing.bLevel > 0) + fUp = FALSE; + + if (!fUp) + DebugMsg(TOPIC_JA2AI, DBG_LEVEL_3, String("Soldier %d is climbing down", pSoldier->ubID)); + + // 0verhaul: Yet another chance to climb the wrong building and otherwise waste CPU power. + // We already know the climb point we want, which may not be here even if climbing is possible. + //if ( CanClimbFromHere ( pSoldier, fUp ) ) + if (pSoldier->sGridNo == sClosestFriend) + { + if (IsActionAffordable(pSoldier) && pSoldier->bActionPoints >= (APBPConstants[AP_CLIMBROOF] + MinAPsToAttack(pSoldier, sClosestFriend, ADDTURNCOST, 0))) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Climb roof"), gLogDecideActionRed); + return(AI_ACTION_CLIMB_ROOF); + } + } + else + { + pSoldier->aiData.usActionData = sClosestFriend; + //INT32 sClimbPoint = FindClosestClimbPoint(pSoldier, pSoldier->sGridNo , sClosestFriend , fUp ); + //if (!TileIsOutOfBounds(sClimbPoint)) + { + //pSoldier->aiData.usActionData = sClimbPoint; + DebugAI(AI_MSG_INFO, pSoldier, String("Move towards climb point"), gLogDecideActionRed); + return(AI_ACTION_MOVE_TO_CLIMB); + } + } + } + //if (fClimb && pSoldier->aiData.usActionData == sClosestFriend) + //{ + // return( AI_ACTION_CLIMB_ROOF ); + //} + DebugAI(AI_MSG_INFO, pSoldier, String("Seek friend"), gLogDecideActionRed); + return(AI_ACTION_SEEK_FRIEND); + } + } + + // mark SEEKING as impossible for next time through while loop +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "couldn't HELP...", 1000); +#endif + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: couldn't help"); + bHelpPts = -99; + } + + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: checking to hide"); + // if HIDING is possible and at least as desirable as seeking or helping + if ((bHidePts > -90) && (bHidePts >= bSeekPts) && (bHidePts >= bHelpPts) && (bHidePts >= bWatchPts)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Take cover]"), gLogDecideActionRed); + //sClosestOpponent = ClosestKnownOpponent( pSoldier, NULL, NULL ); + // if an opponent is known (not necessarily reachable or conscious) + if (!SkipCoverCheck && !TileIsOutOfBounds(sClosestOpponent)) + { + ////////////////////////////////////////////////////////////////////// + // TAKE BEST NEARBY COVER FROM ALL KNOWN OPPONENTS + ////////////////////////////////////////////////////////////////////// +#ifdef AI_TIMING_TESTS + uiStartTime = GetJA2Clock(); +#endif + + pSoldier->aiData.usActionData = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iDummy); +#ifdef AI_TIMING_TESTS + uiEndTime = GetJA2Clock(); + + guiRedHideTimeTotal += (uiEndTime - uiStartTime); + guiRedHideCounter++; +#endif + + // let's be a bit cautious about going right up to a location without enough APs to shoot + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Found a cover spot at %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb); + if (!TileIsOutOfBounds(sClosestDisturbance) && (SpacesAway(pSoldier->aiData.usActionData, sClosestDisturbance) < 5 || SpacesAway(pSoldier->aiData.usActionData, sClosestDisturbance) + 5 < SpacesAway(pSoldier->sGridNo, sClosestDisturbance))) + { + // either moving significantly closer or into very close range + // ensure will we have enough APs for a possible crouch plus a shot + if (InternalGoAsFarAsPossibleTowards(pSoldier, pSoldier->aiData.usActionData, (INT8)(MinAPsToAttack(pSoldier, sClosestOpponent, ADDTURNCOST, 0) + GetAPsCrouch(pSoldier, TRUE)), AI_ACTION_TAKE_COVER, 0) == pSoldier->aiData.usActionData) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Moving to cover, reserve AP for crouch & shot"), gLogDecideActionRed); + return(AI_ACTION_TAKE_COVER); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Moving to cover"), gLogDecideActionRed); + return(AI_ACTION_TAKE_COVER); + } + } + + } + + // mark HIDING as impossible for next time through while loop +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "couldn't HIDE...", 1000); +#endif + + bHidePts = -99; + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: couldn't hide"); + } + } + } + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: nothing to do!"); + //////////////////////////////////////////////////////////////////////////// + // NOTHING USEFUL POSSIBLE! IF NPC IS CURRENTLY UNDER FIRE, TRY TO RUN AWAY + //////////////////////////////////////////////////////////////////////////// + + // if we're currently under fire (presumably, attacker is hidden) + if (pSoldier->aiData.bUnderFire) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Under fire]"), gLogDecideActionRed); + + // only try to run if we've actually been hit recently & noticably so + // otherwise, presumably our current cover is pretty good & sufficient + // HEADROCK HAM B2.6: New value here helps us change the ratio of running away due to shock. This + // is terribly important if Suppression Shock is enabled. + UINT16 bShock = 0; + + if (gGameExternalOptions.usSuppressionShockEffect > 0) + { + // If bShock value is greater than (2*ExpLevel + MoraleModifier)*1.5, the target will flee. + bShock = pSoldier->aiData.bShock; + if (bShock <= ((float)CalcSuppressionTolerance(pSoldier) * (float)1.5)) + bShock = 0; + } + else + { + bShock = pSoldier->aiData.bShock; + } + + if (bShock > 0) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Soldier is shocked, attempt to run away"), gLogDecideActionRed); + // look for best place to RUN AWAY to (farthest from the closest threat) + pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s RUNNING AWAY to grid %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: run away!"); + DebugAI(AI_MSG_INFO, pSoldier, String("Running away to gridno %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + return(AI_ACTION_RUN_AWAY); + } + } + + //////////////////////////////////////////////////////////////////////////// + // UNDER FIRE, DON'T WANNA/CAN'T RUN AWAY, SO CROUCH + //////////////////////////////////////////////////////////////////////////// + + DebugAI(AI_MSG_INFO, pSoldier, String("Under fire, try to change stance"), gLogDecideActionRed); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: crouch or go prone"); + // if not in water and not already crouched + if (gAnimControl[pSoldier->usAnimState].ubHeight == ANIM_STAND && IsValidStance(pSoldier, ANIM_CROUCH)) + { + if (!gfTurnBasedAI || GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints) + { + +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s CROUCHES (STATUS RED)", pSoldier->name); + AIPopMessage(tempstr); +#endif + DebugAI(AI_MSG_INFO, pSoldier, String("Crouching"), gLogDecideActionRed); + + pSoldier->aiData.usActionData = ANIM_CROUCH; + return(AI_ACTION_CHANGE_STANCE); + } + } + else if (gAnimControl[pSoldier->usAnimState].ubHeight != ANIM_PRONE) + { + // maybe go prone + if (PreRandom(2) == 0 && IsValidStance(pSoldier, ANIM_PRONE)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Go prone"), gLogDecideActionRed); + pSoldier->aiData.usActionData = ANIM_PRONE; + return(AI_ACTION_CHANGE_STANCE); + } + } + } + } + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: look around towards opponent"); + //////////////////////////////////////////////////////////////////////////// + // LOOK AROUND TOWARD CLOSEST KNOWN OPPONENT, IF KNOWN + //////////////////////////////////////////////////////////////////////////// + + if (!gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) + { + // determine the location of the known closest opponent + // (don't care if he's conscious, don't care if he's reachable at all) + //sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); + + if (!TileIsOutOfBounds(sClosestOpponent)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Look around towards enemy]"), gLogDecideActionRed); + // determine direction from this soldier to the closest opponent + ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestOpponent); + + // if soldier is not already facing in that direction, + // and the opponent is close enough that he could possibly be seen + // note, have to change this to use the level returned from ClosestKnownOpponent + sDistVisible = pSoldier->GetMaxDistanceVisible(sClosestOpponent, 0, CALC_FROM_ALL_DIRS)*CELL_X_SIZE; + + if ((pSoldier->ubDirection != ubOpponentDir) && (distanceToOpponent <= sDistVisible)) + { + // set base chance according to orders + if ((pSoldier->aiData.bOrders == STATIONARY) || (pSoldier->aiData.bOrders == ONGUARD)) + iChance = 50; + else // all other orders + iChance = 25; + + if (pSoldier->aiData.bAttitude == DEFENSIVE) + iChance += 25; + + if (ARMED_VEHICLE(pSoldier)) + { + iChance += 50; + } + + if ((INT16)PreRandom(100) < iChance && pSoldier->InternalIsValidStance(ubOpponentDir, gAnimControl[pSoldier->usAnimState].ubEndHeight)) + { + pSoldier->aiData.usActionData = ubOpponentDir; + +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s - TURNS TOWARDS CLOSEST ENEMY to face direction %d", pSoldier->name, pSoldier->aiData.usActionData); + AIPopMessage(tempstr); +#endif + DebugAI(AI_MSG_INFO, pSoldier, String("Turn towards closest enemy, face direction %d", pSoldier->aiData.usActionData), gLogDecideActionRed); + if (pSoldier->aiData.bOrders == SNIPER && + !WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION && + (pSoldier->bBreath > 15 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 50)) + { + if (!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, READY_RIFLE_CROUCH) <= pSoldier->bActionPoints) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, sniper"), gLogDecideActionRed); + pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; + } + } + //////////////////////////////////////////////////////////////////////////// + // SANDRO - allow regular soldiers to raise scoped weapons to see rather away too + else if (IsScoped(&pSoldier->inv[HANDPOS])) + { + if (!WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION && + (pSoldier->bBreath > 15 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 50)) + { + if (!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, READY_RIFLE_CROUCH) <= pSoldier->bActionPoints) + { + if (Random(100) < 35) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, scoped weapon"), gLogDecideActionRed); + pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; + } + } + } + } + //////////////////////////////////////////////////////////////////////////// + return(AI_ACTION_CHANGE_FACING); + } + } + //////////////////////////////////////////////////////////////////////////// + // SANDRO - allow regular soldiers to raise scoped weapons to see farther away too + else if (pSoldier->ubDirection == ubOpponentDir && + !WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Facing enemy already"), gLogDecideActionRed); + if ((!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, pSoldier->usAnimState) <= pSoldier->bActionPoints) && (pSoldier->bBreath > 15 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 50)) + { + if (pSoldier->aiData.bOrders == SNIPER) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, sniper"), gLogDecideActionRed); + return AI_ACTION_RAISE_GUN; + } + else if (IsScoped(&pSoldier->inv[HANDPOS])) + { + if (Random(100) < 40) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, scoped weapon"), gLogDecideActionRed); + return AI_ACTION_RAISE_GUN; + } + } + else + { + if (Random(100) < 20) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun"), gLogDecideActionRed); + return AI_ACTION_RAISE_GUN; + } + } + } + } + //////////////////////////////////////////////////////////////////////////// + } + } + + + + //////////////////////////////////////////////////////////////////////////// + // PICKUP A NEARBY ITEM THAT'S USEFUL + //////////////////////////////////////////////////////////////////////////// + + if (ubCanMove && !pSoldier->aiData.bNeutral && (gfTurnBasedAI || pSoldier->bTeam == ENEMY_TEAM)) + { + pSoldier->aiData.bAction = SearchForItems(pSoldier, SEARCH_GENERAL_ITEMS, pSoldier->inv[HANDPOS].usItem); + + // sevenfm: check that location is safe + if (pSoldier->aiData.bAction != AI_ACTION_NONE && + !TileIsOutOfBounds(pSoldier->aiData.usActionData) && + (GetNearestRottingCorpseAIWarning(pSoldier->aiData.usActionData) > 0 || + InLightAtNight(pSoldier->aiData.usActionData, pSoldier->pathing.bLevel) && !InLightAtNight(pSoldier->aiData.usActionData, pSoldier->pathing.bLevel)) && + !fDangerousSpot && + CountFriendsBlack(pSoldier) == 0) + { + // abort! abort! + DebugAI(AI_MSG_INFO, pSoldier, String("Unsafe location, do nothing"), gLogDecideActionRed); + pSoldier->aiData.bAction = AI_ACTION_NONE; + } + + if (pSoldier->aiData.bAction != AI_ACTION_NONE) + { + return(pSoldier->aiData.bAction); + } + } + + + + /* JULY 29, 1996 - Decided that this was a bad idea, after watching a civilian + start a random patrol while 2 steps away from a hidden armed opponent...*/ + + //////////////////////////////////////////////////////////////////////////// + // SWITCH TO GREEN: soldier does ordinary regular patrol, seeks friends + //////////////////////////////////////////////////////////////////////////// + + // if not in combat or under fire, and we COULD have moved, just chose not to + if ((pSoldier->aiData.bAlertStatus != STATUS_BLACK) && !pSoldier->aiData.bUnderFire && ubCanMove && (!gfTurnBasedAI || pSoldier->bActionPoints >= pSoldier->bInitialActionPoints) && (TileIsOutOfBounds(ClosestReachableDisturbance(pSoldier, &fClimb)))) + { + // addition: if soldier is bleeding then reduce bleeding and do nothing + if (pSoldier->bBleeding > MIN_BLEEDING_THRESHOLD) + { + // reduce bleeding by 1 point per AP (in RT, APs will get recalculated so it's okay) + pSoldier->bBleeding = __max(0, pSoldier->bBleeding - (pSoldier->bActionPoints / 2)); + return(AI_ACTION_NONE); // will end-turn/wait depending on whether we're in TB or realtime + } +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "- chose to SKIP all RED actions, BYPASSES to GREEN!", 1000); +#endif + // Skip RED until new situation/next turn, 30% extra chance to do GREEN actions + pSoldier->aiData.bBypassToGreen = 30; + return(DecideActionGreenSoldier(pSoldier)); + } + + + //////////////////////////////////////////////////////////////////////////// + // CROUCH IF NOT CROUCHING ALREADY + //////////////////////////////////////////////////////////////////////////// + + // if not in water and not already crouched, try to crouch down first + if (!bInWater && (gAnimControl[pSoldier->usAnimState].ubHeight == ANIM_STAND) && IsValidStance(pSoldier, ANIM_CROUCH)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Crouch]"), gLogDecideActionRed); + //sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); + + //if ( ( !TileIsOutOfBounds(sClosestOpponent) && PythSpacesAway( pSoldier->sGridNo, sClosestOpponent ) < (MaxNormalDistanceVisible() * 3) / 2 ) || PreRandom( 4 ) == 0 ) + if ((!TileIsOutOfBounds(sClosestOpponent) && distanceToOpponent < (CELL_X_SIZE*pSoldier->GetMaxDistanceVisible(sClosestOpponent) * 3) / 2) || PreRandom(4) == 0) + { + if (!gfTurnBasedAI || GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints) + { + +#ifdef DEBUGDECISIONS + sprintf(tempstr, "%s CROUCHES (STATUS RED)", pSoldier->name); + AIPopMessage(tempstr); +#endif + + //////////////////////////////////////////////////////////////////////////// + // SANDRO - allow regular soldiers to raise scoped weapons to see farther away too + if (!gfTurnBasedAI || (GetAPsToReadyWeapon(pSoldier, READY_RIFLE_CROUCH) + GetAPsToChangeStance(pSoldier, ANIM_CROUCH)) <= pSoldier->bActionPoints) + { + // determine direction from this soldier to the closest opponent + ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestOpponent); + + if (!WeaponReady(pSoldier) && + pSoldier->ubDirection == ubOpponentDir && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) + { + if (IsScoped(&pSoldier->inv[HANDPOS])) + { + if (Random(100) < 40) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, scoped weapon"), gLogDecideActionRed); + pSoldier->aiData.bNextAction = AI_ACTION_RAISE_GUN; + } + } + } + } + //////////////////////////////////////////////////////////////////////////// + + + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance to crouch"), gLogDecideActionRed); + pSoldier->aiData.usActionData = ANIM_CROUCH; + return(AI_ACTION_CHANGE_STANCE); + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // IF UNDER FIRE, FACE THE MOST IMPORTANT NOISE WE KNOW AND GO PRONE + //////////////////////////////////////////////////////////////////////////// + + if (pSoldier->aiData.bUnderFire && pSoldier->bActionPoints >= (pSoldier->bInitialActionPoints - GetAPsToLook(pSoldier)) && IsValidStance(pSoldier, ANIM_PRONE)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Under fire, go prone]"), gLogDecideActionRed); + sClosestDisturbance = MostImportantNoiseHeard(pSoldier, NULL, NULL, NULL); + + if (!TileIsOutOfBounds(sClosestDisturbance)) + { + ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestDisturbance); + if (pSoldier->ubDirection != ubOpponentDir) + { + if (!gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Face direction %d", ubOpponentDir), gLogDecideActionRed); + pSoldier->aiData.usActionData = ubOpponentDir; + return(AI_ACTION_CHANGE_FACING); + } + } + else if ((!gfTurnBasedAI || GetAPsToChangeStance(pSoldier, ANIM_PRONE) <= pSoldier->bActionPoints) && pSoldier->InternalIsValidStance(ubOpponentDir, ANIM_PRONE)) + { + // go prone, end turn + DebugAI(AI_MSG_INFO, pSoldier, String("Go prone & end turn"), gLogDecideActionRed); + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + pSoldier->aiData.usActionData = ANIM_PRONE; + return(AI_ACTION_CHANGE_STANCE); + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // If sniper and nothing else to do then raise gun, and if that doesn't find somebody then goto yellow + //////////////////////////////////////////////////////////////////////////// + if (pSoldier->aiData.bOrders == SNIPER) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Sniper]"), gLogDecideActionRed); + if (pSoldier->sniper == 0) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionRed: sniper raising gun...")); + if ((!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, READY_RIFLE_CROUCH) <= pSoldier->bActionPoints) && (pSoldier->bBreath > 15 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 50)) + { + if (!WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun, sniper"), gLogDecideActionRed); + pSoldier->sniper = 1; + return AI_ACTION_RAISE_GUN; + } + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("Switch to yellow state"), gLogDecideActionRed); + pSoldier->sniper = 0; + return(DecideActionYellowSoldier(pSoldier)); + } + } + else + { + //////////////////////////////////////////////////////////////////////////// + // SANDRO - raise weapon maybe + if (!WeaponReady(pSoldier) && + PickSoldierReadyAnimation(pSoldier, FALSE, FALSE) != INVALID_ANIMATION && + (pSoldier->bBreath > 15 || GetBPCostPer10APsForGunHolding(pSoldier, TRUE) < 50)) + { + if (!gfTurnBasedAI || GetAPsToReadyWeapon(pSoldier, pSoldier->usAnimState) <= pSoldier->bActionPoints) + { + if (IsScoped(&pSoldier->inv[HANDPOS])) + { + if (Random(100) < 35) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Raise gun"), gLogDecideActionRed); + return(AI_ACTION_RAISE_GUN); + } + } + } + } + //////////////////////////////////////////////////////////////////////////// + + } + + //////////////////////////////////////////////////////////////////////////// + // DO NOTHING: Not enough points left to move, so save them for next turn + //////////////////////////////////////////////////////////////////////////// + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionRed: do nothing at all...")); +#ifdef DEBUGDECISIONS + AINameMessage(pSoldier, "- DOES NOTHING (RED)", 1000); +#endif + DebugAI(AI_MSG_INFO, pSoldier, String("Do nothing"), gLogDecideActionRed); + + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); +} + +INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) +{ + DebugAI(AI_MSG_START, pSoldier, String("[Black Soldier]")); + LogDecideInfo(pSoldier); + auto decision = AI_ACTION_INVALID; + + // if we have absolutely no action points, we can't do a thing under BLACK! + if ( pSoldier->bActionPoints <= 0 || pSoldier->IsUnconscious() ) + { + pSoldier->aiData.usActionData = NOWHERE; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_NONE); + } + + //////////////////////////////////////////////////////////////////////////// + // Prepare data + //////////////////////////////////////////////////////////////////////////// + pSoldier->bStealthMode = FALSE; // sevenfm: disable stealth mode + pSoldier->bReverse = FALSE; // disable reverse movement mode + pSoldier->bWeaponMode = WM_NORMAL; // sevenfm: initialize data + + // World knowledge + INT32 sOpponentGridNo; + INT8 bOpponentLevel; + INT32 distanceToOpponent; + INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel, NULL, &distanceToOpponent); + DebugAI(AI_MSG_INFO, pSoldier, String("sClosestOpponent %d", sClosestOpponent)); + + + const BOOLEAN fProneSightCover = ProneSightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE); + const BOOLEAN fAnyCover = AnyCoverAtSpot(pSoldier, pSoldier->sGridNo); + //const BOOLEAN fDangerousSpot = DangerousSpot(pSoldier); + //const BOOLEAN fSafeSpot = SafeSpot(pSoldier); + + BOOLEAN fDangerousSpot = FALSE; + if ( !fProneSightCover || (pSoldier->aiData.bUnderFire && !fAnyCover) ) + { + fDangerousSpot = TRUE; + } + + DebugAI(AI_MSG_INFO, pSoldier, String("prone sight cover %d", fProneSightCover), gLogDecideActionBlack); + DebugAI(AI_MSG_INFO, pSoldier, String("any cover %d", fAnyCover), gLogDecideActionBlack); + + + // Do commonly used checks in advance + // can this guy move to any of the neighbouring squares ? (sets TRUE/FALSE) + const bool ubCanMove = (pSoldier->bActionPoints >= MinPtsToMove(pSoldier) && !(pSoldier->flags.uiStatusFlags & (SOLDIER_DRIVER | SOLDIER_PASSENGER))); + const bool canFunction = (pSoldier->stats.bLife >= OKLIFE && !pSoldier->bCollapsed && !pSoldier->bBreathCollapsed); + + + //////////////////////////////////////////////////////////////////////////// + // Start evaluating decisions + //////////////////////////////////////////////////////////////////////////// + + // sevenfm: stop flanking when we see enemy + if ( AICheckIsFlanking(pSoldier) ) + { + pSoldier->numFlanks = 0; + } + + // Before deciding anything, stop cowering + if (ubCanMove && canFunction && pSoldier->IsCowering()) + { + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d stop cowering", pSoldier->ubID.i); + return AI_ACTION_STOP_COWERING; + } + + // sevenfm: stop giving aid + if (pSoldier->bActionPoints > 0 && canFunction && pSoldier->IsGivingAid()) + { + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d stop giving medical aid", pSoldier->ubID.i); + return AI_ACTION_STOP_MEDIC; + } + + if ((pSoldier->bTeam == ENEMY_TEAM || pSoldier->ubProfile == WARDEN) && (gTacticalStatus.fPanicFlags & PANIC_TRIGGERS_HERE) && (gTacticalStatus.ubTheChosenOne == NOBODY)) + { + INT8 bPanicTrigger = ClosestPanicTrigger(pSoldier); + // if it's an alarm trigger and team is alerted, ignore it + if (bPanicTrigger != -1 && !(gTacticalStatus.bPanicTriggerIsAlarm[bPanicTrigger] && gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition) && PythSpacesAway(pSoldier->sGridNo, gTacticalStatus.sPanicTriggerGridNo[bPanicTrigger]) < 10) + { + PossiblyMakeThisEnemyChosenOne(pSoldier); + } + } + + // if this soldier is the "Chosen One" (enemies only) + if (pSoldier->ubID == gTacticalStatus.ubTheChosenOne) + { + // do some special panic AI decision making + decision = PanicAI(pSoldier, ubCanMove); + + // if we decided on an action while in there, we're done + if (decision != AI_ACTION_INVALID) + return(decision); + } + + if (pSoldier->ubProfile != NO_PROFILE) + { + // if they see enemies, the Queen will keep going to the staircase, but Joe will fight + if ((pSoldier->ubProfile == QUEEN) && ubCanMove) + { + if (gWorldSectorX == 3 && gWorldSectorY == MAP_ROW_P && gbWorldSectorZ == 0 && !gfUseAlternateQueenPosition) + { + decision = HeadForTheStairCase(pSoldier); + if ( decision != AI_ACTION_NONE) + { + return(decision); + } + } + } + } + + // determine if we happen to be in water (in which case we're in BIG trouble!) + INT8 bInWater = Water(pSoldier->sGridNo, pSoldier->pathing.bLevel); + INT8 bInDeepWater = WaterTooDeepForAttacks(pSoldier->sGridNo, pSoldier->pathing.bLevel); + INT8 bInGas = DecideActionWearGasmask(pSoldier); + + pSoldier->aiData.bAIMorale = CalcMorale(pSoldier); + + + //////////////////////////////////////////////////////////////////////////// + // IF GASSED, OR REALLY TIRED (ON THE VERGE OF COLLAPSING), TRY TO RUN AWAY + //////////////////////////////////////////////////////////////////////////// + + // if we're desperately short on breath (it's OK if we're in water, though!) + if (bInGas || (pSoldier->bBreath < OKBREATH)) + { + // if soldier has enough APs left to move at least 1 square's worth + if (ubCanMove) + { + // look for best place to RUN AWAY to (farthest from the closest threat) + pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Gassed or low on breath, run away to grid %d", pSoldier->aiData.usActionData)); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d in gas or low on breath, run away to grid %d", pSoldier->ubID.i, pSoldier->aiData.usActionData); + return(AI_ACTION_RUN_AWAY); + } + } + + // if not already crouched, try to crouch down first + if ( !PTR_CROUCHED && IsValidStance(pSoldier, ANIM_CROUCH) && + gAnimControl[pSoldier->usAnimState].ubEndHeight > ANIM_CROUCH ) + { + if ( GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints ) + { + pSoldier->aiData.usActionData = ANIM_CROUCH; + DebugAI(AI_MSG_TOPIC, pSoldier, String("crouch first")); + return(AI_ACTION_CHANGE_STANCE); + } + } + + // REALLY tired, can't get away, force soldier's morale to hopeless state + if (gGameOptions.ubDifficultyLevel == DIF_LEVEL_INSANE) + { + pSoldier->bBreath = pSoldier->bBreathMax; //Madd: backed into a corner, so go crazy like a wild animal... + pSoldier->aiData.bAIMorale = MORALE_FEARLESS; + } + else + pSoldier->aiData.bAIMorale = MORALE_HOPELESS; + } + + + + //////////////////////////////////////////////////////////////////////////// + // STUCK IN WATER OR GAS, NO COVER, GO TO NEAREST SPOT OF UNGASSED LAND + //////////////////////////////////////////////////////////////////////////// + decision = DecideActionStuckInWaterOrGas(pSoldier, ubCanMove, bInWater, bInDeepWater, bInGas); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + + + //////////////////////////////////////////////////////////////////////// + // Offer surrender? + //////////////////////////////////////////////////////////////////////// +#ifndef JA2UB + if (pSoldier->bTeam == ENEMY_TEAM && pSoldier->bVisible == TRUE && !(gTacticalStatus.fEnemyFlags & ENEMY_OFFERED_SURRENDER) && pSoldier->stats.bLife >= pSoldier->stats.bLifeMax / 2) + { + if (gTacticalStatus.Team[MILITIA_TEAM].bMenInSector == 0 && gTacticalStatus.Team[CREATURE_TEAM].bMenInSector == 0 && NumPCsInSector() < 4 && gTacticalStatus.Team[ENEMY_TEAM].bMenInSector >= NumPCsInSector() * 3) + { + if (gubQuest[QUEST_HELD_IN_ALMA] == QUESTNOTSTARTED || gubQuest[QUEST_HELD_IN_TIXA] == QUESTNOTSTARTED || gubQuest[QUEST_INTERROGATION] == QUESTNOTSTARTED) + { + return(AI_ACTION_OFFER_SURRENDER); + } + } + } +#endif + + + //////////////////////////////////////////////////////////////////////// + // WAIT IF BEING BANDAGED + //////////////////////////////////////////////////////////////////////// + //BOOLEAN fStopMovement = FALSE; + //DebugAI(AI_MSG_TOPIC, pSoldier, String("[Being bandaged]")); + // if ( AICheckIsBandaged(pSoldier) && + // !bInWater && + // !bInGas && + // !pSoldier->aiData.bUnderFire && + // fSafeSpot ) + // { + // DebugAI(AI_MSG_INFO, pSoldier, String("ubServiceCount %d, stop movement", pSoldier->ubServiceCount)); + // fStopMovement = TRUE; + // } + + + //////////////////////////////////////////////////////////////////////////// + // SOLDIER CAN ATTACK IF NOT IN WATER/GAS AND NOT DOING SOMETHING TOO FUNKY + //////////////////////////////////////////////////////////////////////////// + ATTACKTYPE BestShot, BestThrow, BestStab, BestAttack; + INT8 bCanAttack = FALSE; + BOOLEAN fTryPunching = FALSE; + + // NPCs in water/tear gas without masks are not permitted to shoot/stab/throw + if (!bInDeepWater && !bInGas) + { + do + { + bCanAttack = CanNPCAttack(pSoldier); + if (bCanAttack != TRUE) + { + if (bCanAttack == NOSHOOT_NOAMMO && ubCanMove && !pSoldier->aiData.bNeutral) + { + int handPOS; + //CHRISL: We need to know which weapon has no ammo in case the soldier is holding a weapoin in SECONDHANDPOS + if (pSoldier->inv[SECONDHANDPOS].exists() == true && pSoldier->inv[SECONDHANDPOS][0]->data.gun.ubGunShotsLeft == 0) + handPOS = SECONDHANDPOS; + else + handPOS = HANDPOS; + + // try to find more ammo + pSoldier->aiData.bAction = SearchForItems(pSoldier, SEARCH_AMMO, pSoldier->inv[handPOS].usItem); + + if (pSoldier->aiData.bAction == AI_ACTION_NONE) + { + // the current weapon appears is useless right now! + // (since we got a return code of noammo, we know the hand usItem + // is our gun) + pSoldier->inv[handPOS].fFlags |= OBJECT_AI_UNUSABLE; + // move the gun into another pocket... + if (!AutoPlaceObject(pSoldier, &(pSoldier->inv[handPOS]), FALSE)) + { + // If there's no room in his pockets for the useless gun, just throw it away + return AI_ACTION_DROP_ITEM; + } + } + else + { + return(pSoldier->aiData.bAction); + } + } + else + { + bCanAttack = FALSE; + } + } + } while (bCanAttack != TRUE && bCanAttack != FALSE); + + if (!bCanAttack) + { + if (pSoldier->aiData.bAIMorale > MORALE_WORRIED) + { + pSoldier->aiData.bAIMorale = MORALE_WORRIED; + } + + // can always attack with HTH as a last resort + bCanAttack = TRUE; + fTryPunching = TRUE; + } + } + + //////////////////////////////////////////////////////////////////////////// + // THROW A SMOKE GRENADE FOR COVER + //////////////////////////////////////////////////////////////////////////// + if (gfTurnBasedAI && + pSoldier->bActionPoints == pSoldier->bInitialActionPoints && + pSoldier->aiData.bUnderFire && + !InARoom(pSoldier->sGridNo, NULL) && + !InSmoke(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + RangeChangeDesire(pSoldier) <= 2 && + (!NightLight() || InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel)) && + !TileIsOutOfBounds(sClosestOpponent) && + distanceToOpponent > 10*TACTICAL_RANGE / 4 && + (!fProneSightCover && !fAnyCover || pSoldier->TakenLargeHit()) && + (pSoldier->TakenLargeHit() || pSoldier->ShockLevelPercent() > 20 + Random(80))) + { + DebugAI(AI_MSG_INFO, pSoldier, String("check if soldier can cover himself with smoke")); + + CheckTossSelfSmoke(pSoldier, &BestThrow); + + if (BestThrow.ubPossible) + { + DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime)); + + // start retreating for several turns + pSoldier->RetreatCounterStart(3); + + // if necessary, swap the usItem from holster into the hand position + if (BestThrow.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket")); + RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); + } + + // stand up before throwing if needed + if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && + pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) + { + pSoldier->aiData.usActionData = BestThrow.ubStance; + pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; + pSoldier->aiData.usNextActionData = BestThrow.sTarget; + pSoldier->aiData.bNextTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d stand up before throwing smoke grenade for cover. Target stance %d", pSoldier->ubID.i, pSoldier->aiData.usActionData); + return AI_ACTION_CHANGE_STANCE; + } + else + { + pSoldier->aiData.usActionData = BestThrow.sTarget; + pSoldier->bTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + } + + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d throw smoke grenade for cover. Target grid %d", pSoldier->ubID.i, pSoldier->aiData.usActionData); + return(AI_ACTION_TOSS_PROJECTILE); + } + } + + + //////////////////////////////////////////////////////////////////////////// + // LOOK FOR A WEAPON + //////////////////////////////////////////////////////////////////////////// + // if we don't have a gun, look around for a weapon! + if (FindAIUsableObjClass(pSoldier, IC_GUN) == ITEM_NOT_FOUND && ubCanMove && !pSoldier->aiData.bNeutral) + { + // look around for a gun... + pSoldier->aiData.bAction = SearchForItems(pSoldier, SEARCH_WEAPONS, pSoldier->inv[HANDPOS].usItem); + if (pSoldier->aiData.bAction != AI_ACTION_NONE) + { + return(pSoldier->aiData.bAction); + } + } + + + //////////////////////////////////////////////////////////////////////////// + // RADIO OPERATOR TRAIT + //////////////////////////////////////////////////////////////////////////// + decision = DecideActionRadioOperator(pSoldier, gLogDecideActionBlack); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + + + //////////////////////////////////////////////////////////////////////////// + // VIP RETREAT + //////////////////////////////////////////////////////////////////////////// + // VIPs run away (but not the GENERAL) + decision = DecideActionVIPretreat(pSoldier, gLogDecideActionBlack); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + + //////////////////////////////////////////////////////////////////////////// + // DETERMINE BEST ATTACK + //////////////////////////////////////////////////////////////////////////// + BestShot.ubPossible = FALSE; // by default, assume Shooting isn't possible + BestThrow.ubPossible = FALSE; // by default, assume Throwing isn't possible + BestStab.ubPossible = FALSE; // by default, assume Stabbing isn't possible + BestAttack.ubChanceToReallyHit = 0; + UINT8 ubBestAttackAction = AI_ACTION_NONE; + + // if we are able attack + if (bCanAttack) + { + pSoldier->bAimShotLocation = AIM_SHOT_RANDOM; + + ////////////////////////////////////////////////////////////////////////// + // FIRE A GUN AT AN OPPONENT + ////////////////////////////////////////////////////////////////////////// + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "FIRE A GUN AT AN OPPONENT"); + + //CheckIfShotPossible(pSoldier, &BestShot); + extern void CheckIfShotsPossible(SOLDIERTYPE * pSoldier, std::vector&possibleShots); + std::vector possibleShots{}; + CheckIfShotsPossible(pSoldier, possibleShots); + + if (possibleShots.size() > 0) + { + if (possibleShots.size() > 1) + { + std::vector edges{}; + WeighAttacks(possibleShots, edges); + auto choice = DoWeightedChoice(edges); + + DebugAI(AI_MSG_INFO, pSoldier, String("Chose option %d out of all possible %d", choice, possibleShots.size())); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d chose shot option %d out of all possible %d", pSoldier->ubID.i, choice, possibleShots.size()); + BestShot = possibleShots[choice]; + } + else + { + BestShot = possibleShots[0]; + } + } + + if (BestShot.ubFriendlyFireChance) //dnl ch61 180813 + { + // determine chance to shoot + INT32 iChanceToShoot; + + iChanceToShoot = 100 - BestShot.ubFriendlyFireChance; + iChanceToShoot = iChanceToShoot * iChanceToShoot / 100; + + DebugAI(AI_MSG_INFO, pSoldier, String("Friendly fire chance %d, chance to shoot %d", BestShot.ubFriendlyFireChance, iChanceToShoot)); + + if (Chance(100 - iChanceToShoot)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Friendly fire check failed, skip shooting!")); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d decided not to shoot due to FF chance %d, chance to shoot %d", pSoldier->ubID.i, BestShot.ubFriendlyFireChance, iChanceToShoot); + BestShot.ubPossible = FALSE; + } + } + + if (BestShot.ubPossible) + { + // if the selected opponent is not a threat (unconscious & !serviced) + // (usually, this means all the guys we see are unconscious, but, on + // rare occasions, we may not be able to shoot a healthy guy, too) + if ((BestShot.ubOpponent->stats.bLife < OKLIFE) && !BestShot.ubOpponent->bService && + (pSoldier->aiData.bAttitude != AGGRESSIVE || Chance((100 - BestShot.ubChanceToReallyHit) / 2))) + { + // get the location of the closest CONSCIOUS reachable opponent + BOOLEAN fClimbDummy; + INT32 sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimbDummy); + + // if we found one + if (!TileIsOutOfBounds(sClosestDisturbance)) + { + // then make decision as if at alert status RED + return DecideActionRedSoldier(pSoldier); + } + // else kill the guy, he could be the last opponent alive in this sector + } + + // now we KNOW FOR SURE that we will do something (shoot, at least) + NPCDoesAct(pSoldier); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "NPC decided to shoot (or something)"); + } + + ////////////////////////////////////////////////////////////////////////// + // THROW A TOSSABLE ITEM AT OPPONENT(S) + // - HTH: THIS NOW INCLUDES FIRING THE GRENADE LAUNCHAR AND MORTAR! + ////////////////////////////////////////////////////////////////////////// + + // this looks for throwables, and sets BestThrow.ubPossible if it can be done + CheckIfTossPossible(pSoldier, &BestThrow); + + if (BestThrow.ubPossible) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "good throw possible"); + if (ItemIsMortar(pSoldier->inv[BestThrow.bWeaponIn].usItem)) + { + UINT8 ubOpponentDir = AIDirection(pSoldier->sGridNo, BestThrow.sTarget); + + // Get new gridno + INT32 sCheckGridNo = NewGridNo(pSoldier->sGridNo, (UINT16)DirectionInc(ubOpponentDir)); + + if (!OKFallDirection(pSoldier, sCheckGridNo, pSoldier->pathing.bLevel, ubOpponentDir, pSoldier->usAnimState)) + { + // can't fire! + BestThrow.ubPossible = FALSE; + + // try behind us, see if there's room to move back + sCheckGridNo = NewGridNo(pSoldier->sGridNo, (UINT16)DirectionInc(gOppositeDirection[ubOpponentDir])); + if (OKFallDirection(pSoldier, sCheckGridNo, pSoldier->pathing.bLevel, gOppositeDirection[ubOpponentDir], pSoldier->usAnimState)) + { + // sevenfm: check if we can reach this gridno + INT32 iPathCost = EstimatePlotPath(pSoldier, sCheckGridNo, FALSE, FALSE, FALSE, DetermineMovementMode(pSoldier, AI_ACTION_GET_CLOSER), pSoldier->bStealthMode, FALSE, 0); + if (iPathCost != 0 && iPathCost <= pSoldier->bActionPoints) + { + pSoldier->aiData.usActionData = sCheckGridNo; + return AI_ACTION_GET_CLOSER; + } + } + } + } + + if (BestThrow.ubPossible) + { + // now we KNOW FOR SURE that we will do something (throw, at least) + NPCDoesAct(pSoldier); + } + } + + ////////////////////////////////////////////////////////////////////////// + // GO STAB AN OPPONENT WITH A KNIFE + ////////////////////////////////////////////////////////////////////////// + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "GO STAB AN OPPONENT WITH A KNIFE"); + // if soldier has a knife in his hand + INT8 bWeaponIn = FindAIUsableObjClass(pSoldier, (IC_BLADE | IC_THROWING_KNIFE)); + + // if the soldier does have a usable knife somewhere + // 0verhaul: And is not a tank! + if (bWeaponIn != NO_SLOT && !(pSoldier->flags.uiStatusFlags & (SOLDIER_DRIVER | SOLDIER_PASSENGER))) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "try to stab"); + BestStab.bWeaponIn = bWeaponIn; + // if it's in his holster, swap it into his hand temporarily + if (bWeaponIn != HANDPOS) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionblack: about to rearrange pocket before stab check"); + RearrangePocket(pSoldier, HANDPOS, bWeaponIn, TEMPORARILY); + } + + // get the minimum cost to attack with this knife + INT16 ubMinAPCost = MinAPsToAttack(pSoldier, pSoldier->sLastTarget, DONTADDTURNCOST, 0, 0); + + // if we can afford the minimum AP cost to stab with/throw this knife weapon + if (pSoldier->bActionPoints >= ubMinAPCost) + { + // NB throwing knife in hand now + if (Item[pSoldier->inv[HANDPOS].usItem].usItemClass & IC_THROWING_KNIFE) + { + // throwing knife code works like shooting + + // look around for a worthy target (which sets BestStab.ubPossible) + CalcBestShot(pSoldier, &BestStab); + + if (BestStab.ubPossible) + { + // if the selected opponent is not a threat (unconscious & !serviced) + // (usually, this means all the guys we see are unconscious, but, on + // rare occasions, we may not be able to shoot a healthy guy, too) + if ((BestStab.ubOpponent->stats.bLife < OKLIFE) && !BestStab.ubOpponent->bService) + { + // don't throw a knife at him. + BestStab.ubPossible = FALSE; + } + + if ( BestStab.ubPossible ) + { + // now we KNOW FOR SURE that we will do something (shoot, at least) + NPCDoesAct(pSoldier); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "NPC decided to throw a knife"); + } + } + } + else + { + //sprintf((CHAR *)tempstr,"%s - ubMinAPCost = %d",pSoldier->name,ubMinAPCost); + //PopMessage(tempstr); + // then look around for a worthy target (which sets BestStab.ubPossible) + CalcBestStab(pSoldier, &BestStab, TRUE); + + if (BestStab.ubPossible) + { + INT32 sAttackDist = PythSpacesAway(pSoldier->sGridNo, BestStab.sTarget); + INT32 sMaxStabAttackDist = TACTICAL_RANGE / 8; + // sevenfm: limit stab attacks when target is not very close + if (sAttackDist > sMaxStabAttackDist) + { + BestStab.iAttackValue = BestStab.iAttackValue * sMaxStabAttackDist / sAttackDist; + } + + if (!(gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSoldier, MELEE_NT)) && + !(!gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSoldier, KNIFING_OT))) + { + BestStab.iAttackValue /= 4; + } + + // sevenfm: reduce stab attack attractiveness depending on number of seen opponents + if (pSoldier->aiData.bOppCnt > 1) + { + BestStab.iAttackValue /= pSoldier->aiData.bOppCnt; + } + + // now we KNOW FOR SURE that we will do something (stab, at least) + NPCDoesAct(pSoldier); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "NPC decided to stab"); + } + } + + } + + // if it was in his holster, swap it back into his holster for now + if (bWeaponIn != HANDPOS) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "about to rearrange pocket after stab check"); + RearrangePocket(pSoldier, HANDPOS, bWeaponIn, TEMPORARILY); + } + } + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // SANDRO - even if we don't have any blade, calculate how much damage we could do unarmed + else if (!(pSoldier->flags.uiStatusFlags & (SOLDIER_DRIVER | SOLDIER_PASSENGER))) + { + bWeaponIn = FindAIUsableObjClass(pSoldier, IC_PUNCH); + if (bWeaponIn == NO_SLOT) // if no punch-type weapon found, just calculate it with empty hands + { + bWeaponIn = FindEmptySlotWithin(pSoldier, HANDPOS, NUM_INV_SLOTS); + } + if (bWeaponIn != NO_SLOT) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "try to punch"); + BestStab.bWeaponIn = bWeaponIn; + // if it's in his holster, swap it into his hand temporarily + if (bWeaponIn != HANDPOS) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionblack: about to rearrange pocket before punch check"); + RearrangePocket(pSoldier, HANDPOS, bWeaponIn, TEMPORARILY); + } + + // get the minimum cost to attack with punch + INT16 ubMinAPCost = MinAPsToAttack(pSoldier, pSoldier->sLastTarget, DONTADDTURNCOST, 0, 0); + // if we can afford the minimum AP cost to punch + if (pSoldier->bActionPoints >= ubMinAPCost) + { + // then look around for a worthy target (which sets BestStab.ubPossible) + CalcBestStab(pSoldier, &BestStab, FALSE); + + if (BestStab.ubPossible) + { + // if we have not enough APs to deal at least two or three punches, + // reduce the attack value as one punch ain't much + if (gGameOptions.fNewTraitSystem) + { + // if we are not specialized, reduce the attack attractiveness generally + if (!HAS_SKILL_TRAIT(pSoldier, MARTIAL_ARTS_NT)) + { + BestStab.iAttackValue /= 4; + // if too far and not having APs for at least 3 hits no way to attack + if (((CalcTotalAPsToAttack(pSoldier, BestStab.sTarget, ADDTURNCOST, 0) + (2 * (ApsToPunch(pSoldier)))) > pSoldier->bActionPoints) && !(PythSpacesAway(pSoldier->sGridNo, BestStab.sTarget) <= 1)) + { + BestStab.ubPossible = 0; + BestStab.iAttackValue = 0; + } + } + else + { + if (PythSpacesAway(pSoldier->sGridNo, BestStab.sTarget) <= 1) + { + BestStab.iAttackValue = (BestStab.iAttackValue * 2); + } + // if too far and not having APs for at least 2 hits + else if (((CalcTotalAPsToAttack(pSoldier, BestStab.sTarget, ADDTURNCOST, 0) + ApsToPunch(pSoldier)) > pSoldier->bActionPoints) && !(PythSpacesAway(pSoldier->sGridNo, BestStab.sTarget) <= 1)) + { + BestStab.iAttackValue /= 3; + } + } + } + else + { + if (!HAS_SKILL_TRAIT(pSoldier, MARTIALARTS_OT) && !HAS_SKILL_TRAIT(pSoldier, HANDTOHAND_OT)) + { + // if we are not specialized, reduce the attack attractiveness generally + BestStab.iAttackValue /= 4; + // if too far and not having APs for at least 3 hits + if (((CalcTotalAPsToAttack(pSoldier, BestStab.sTarget, ADDTURNCOST, 0) + (2 * (ApsToPunch(pSoldier)))) > pSoldier->bActionPoints) && !(PythSpacesAway(pSoldier->sGridNo, BestStab.sTarget <= 1))) + { + BestStab.ubPossible = 0; + BestStab.iAttackValue = 0; + } + } + else + { + BestStab.iAttackValue = ((BestStab.iAttackValue * 3) / 2); + + if (PythSpacesAway(pSoldier->sGridNo, BestStab.sTarget) <= 1) + { + BestStab.iAttackValue = ((BestStab.iAttackValue * 3) / 2); + } + // if too far and not having APs for at least 2 hits + else if (((CalcTotalAPsToAttack(pSoldier, BestStab.sTarget, ADDTURNCOST, 0) + ApsToPunch(pSoldier)) > pSoldier->bActionPoints) && !(PythSpacesAway(pSoldier->sGridNo, BestStab.sTarget <= 1))) + { + BestStab.iAttackValue /= 3; + } + } + } + + // sevenfm: reduce HTH attack attractiveness depending on number of seen opponents + if (pSoldier->aiData.bOppCnt > 1) + { + BestStab.iAttackValue /= pSoldier->aiData.bOppCnt; + } + + // now we KNOW FOR SURE that we will do something (stab, at least) + NPCDoesAct(pSoldier); + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "NPC decided to punch"); + } + + } + // if it was in his holster, swap it back into his holster for now + if (bWeaponIn != HANDPOS) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "about to rearrange pocket after punch check"); + RearrangePocket(pSoldier, HANDPOS, bWeaponIn, TEMPORARILY); + } + } + } + ///////////////////////////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////// + // CHOOSE THE BEST TYPE OF ATTACK OUT OF THOSE FOUND TO BE POSSIBLE + ////////////////////////////////////////////////////////////////////////// + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "CHOOSE THE BEST TYPE OF ATTACK OUT OF THOSE FOUND TO BE POSSIBLE"); + BestAttack.iAttackValue = 0; + + if (BestShot.ubPossible) + { + BestAttack.iAttackValue = BestShot.iAttackValue; + ubBestAttackAction = AI_ACTION_FIRE_GUN; + DebugAI(AI_MSG_INFO, pSoldier, String("best action = fire gun, iAttackValue = %d", BestAttack.iAttackValue)); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d best attack = fire gun, iAttackValue = %d, target grid %d", pSoldier->ubID.i, BestShot.iAttackValue, BestShot.ubOpponent); + } + + if (BestStab.ubPossible && ((BestStab.iAttackValue > BestAttack.iAttackValue) || (ubBestAttackAction == AI_ACTION_NONE))) + { + BestAttack.iAttackValue = BestStab.iAttackValue; + if (Item[pSoldier->inv[BestStab.bWeaponIn].usItem].usItemClass & IC_THROWING_KNIFE) + { + ubBestAttackAction = AI_ACTION_THROW_KNIFE; + DebugAI(AI_MSG_INFO, pSoldier, String("best action = throw knife, iAttackValue = %d", BestAttack.iAttackValue)); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d best attack = throw knife, iAttackValue = %d, target grid %d", pSoldier->ubID.i, BestStab.iAttackValue, BestStab.ubOpponent); + } + else if (Item[pSoldier->inv[BestStab.bWeaponIn].usItem].usItemClass & IC_BLADE) // SANDRO - check specifically for blade attack + { + ubBestAttackAction = AI_ACTION_KNIFE_MOVE; + DebugAI(AI_MSG_INFO, pSoldier, String("best action = move to stab, iAttackValue = %d", BestAttack.iAttackValue)); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d best attack = move to stab, iAttackValue = %d, target grid %d", pSoldier->ubID.i, BestStab.iAttackValue, BestStab.ubOpponent); + } + //////////////////////////////////////////////////////////////////////////////////// + // SANDRO - added a chance to try to steal merc's gun from hands + else + { + if (AIDetermineStealingWeaponAttempt(pSoldier, BestStab.ubOpponent) == TRUE) + { + ubBestAttackAction = AI_ACTION_STEAL_MOVE; + DebugAI(AI_MSG_INFO, pSoldier, String("best action = move to steal weapon, iAttackValue = %d", BestStab.iAttackValue)); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d best attack = move to steal weapon, iAttackValue = %d, target grid %d", pSoldier->ubID.i, BestStab.iAttackValue, BestStab.ubOpponent); + } + else + { + ubBestAttackAction = AI_ACTION_KNIFE_MOVE; + DebugAI(AI_MSG_INFO, pSoldier, String("best action = knife move, iAttackValue = %d", BestStab.iAttackValue)); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d best attack = knife move, iAttackValue = %d, target grid %d", pSoldier->ubID.i, BestStab.iAttackValue, BestStab.ubOpponent); + } + } + //////////////////////////////////////////////////////////////////////////////////// + } + if ( BestThrow.ubPossible && ((BestThrow.iAttackValue > BestAttack.iAttackValue) || (ubBestAttackAction == AI_ACTION_NONE)) ) + { + ubBestAttackAction = AI_ACTION_TOSS_PROJECTILE; + DebugAI(AI_MSG_INFO, pSoldier, String("best action = throw something, iAttackValue = %d", BestThrow.iAttackValue)); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d best attack = throw something, iAttackValue = %d, target grid %d", pSoldier->ubID.i, BestThrow.iAttackValue, BestThrow.ubOpponent); + } + + if ((ubBestAttackAction == AI_ACTION_NONE) && fTryPunching) + { + // nothing (else) to attack with so let's try hand-to-hand + bWeaponIn = FindObj(pSoldier, NOTHING, HANDPOS, NUM_INV_SLOTS); + + if (bWeaponIn != NO_SLOT) + { + BestStab.bWeaponIn = bWeaponIn; + // if it's in his holster, swap it into his hand temporarily + if (bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("swap knife into hand")); + RearrangePocket(pSoldier, HANDPOS, bWeaponIn, TEMPORARILY); + } + + // get the minimum cost to attack by HTH + INT16 ubMinAPCost = MinAPsToAttack(pSoldier, pSoldier->sLastTarget, DONTADDTURNCOST, 0, 0); + + // if we can afford the minimum AP cost to use HTH combat + if (pSoldier->bActionPoints >= ubMinAPCost) + { + // then look around for a worthy target (which sets BestStab.ubPossible) + CalcBestStab(pSoldier, &BestStab, FALSE); + + if (BestStab.ubPossible) + { + // now we KNOW FOR SURE that we will do something (stab, at least) + NPCDoesAct(pSoldier); + ubBestAttackAction = AI_ACTION_KNIFE_MOVE; + DebugAI(AI_MSG_INFO, pSoldier, String("best action = move to HtH, iAttackValue = %d", BestStab.iAttackValue)); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d best attack = HtH, iAttackValue = %d, target grid %d", pSoldier->ubID.i, BestAttack.iAttackValue, BestAttack.ubOpponent); + } + } + + // if it was in his holster, swap it back into his holster for now + if (bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("put knife away")); + RearrangePocket(pSoldier, HANDPOS, bWeaponIn, TEMPORARILY); + } + } + } + + // copy the information on the best action selected into BestAttack struct + DebugAI(AI_MSG_INFO, pSoldier, String("copy the information on the best action selected into BestAttack struct")); + switch (ubBestAttackAction) + { + case AI_ACTION_FIRE_GUN: + memcpy(&BestAttack, &BestShot, sizeof(BestAttack)); + DebugAI(AI_MSG_INFO, pSoldier, String("Best attack - shooting")); + break; + + case AI_ACTION_TOSS_PROJECTILE: + memcpy(&BestAttack, &BestThrow, sizeof(BestAttack)); + DebugAI(AI_MSG_INFO, pSoldier, String("Best attack - throwing grenade")); + break; + + case AI_ACTION_THROW_KNIFE: + case AI_ACTION_KNIFE_MOVE: + DebugAI(AI_MSG_INFO, pSoldier, String("Best attack - stab")); + memcpy(&BestAttack, &BestStab, sizeof(BestAttack)); + break; + case AI_ACTION_STEAL_MOVE: // added by SANDRO + DebugAI(AI_MSG_INFO, pSoldier, String("Best attack - steal weapon")); + memcpy(&BestAttack, &BestStab, sizeof(BestAttack)); + break; + + default: + // set to empty + DebugAI(AI_MSG_INFO, pSoldier, String("Best attack - no good attack")); + memset(&BestAttack, 0, sizeof(BestAttack)); + break; + } + } + + const UINT16 usRange = BestAttack.bWeaponIn == NO_SLOT ? 0 : GetModifiedGunRange(pSoldier->inv[BestAttack.bWeaponIn].usItem);//dnl ch69 150913 + const bool targetFarAway = PythSpacesAway(pSoldier->sGridNo, BestAttack.sTarget) > usRange / CELL_X_SIZE; // Out of effective range + + + ////////////////////////////////////////////////////////////////////////// + // STATUS BLACK RETREAT + ////////////////////////////////////////////////////////////////////////// + if (gfTurnBasedAI && + !bInWater && + ubCanMove && + !gfHiddenInterrupt && + !gTacticalStatus.fInterruptOccurred && + pSoldier->aiData.bOrders != STATIONARY && + pSoldier->aiData.bOrders != SNIPER && + pSoldier->RetreatCounterValue() > 0 && + (ubBestAttackAction == AI_ACTION_NONE || ubBestAttackAction == AI_ACTION_FIRE_GUN && (UINT8)BestAttack.ubChanceToReallyHit < Random(10 + pSoldier->ShockLevelPercent() / 4)) && + (pSoldier->CheckInitialAP() || !fAnyCover || pSoldier->aiData.bUnderFire)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Black Retreat]")); + INT32 sRetreatSpot = FindRetreatSpot(pSoldier); + + if (!TileIsOutOfBounds(sRetreatSpot)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("found retreat spot %d", sRetreatSpot)); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d status black retreat, target grid %d", pSoldier->ubID.i, sRetreatSpot); + + //BeginMultiPurposeLocator(sRetreatSpot, pSoldier->pathing.bLevel, FALSE); + + pSoldier->aiData.usActionData = sRetreatSpot; + return(AI_ACTION_TAKE_COVER); + } + } + + + ////////////////////////////////////////////////////////////////////////// + // STATUS BLACK ADVANCE TO COVER + ////////////////////////////////////////////////////////////////////////// + if (gfTurnBasedAI && + pSoldier->bInitialActionPoints > APBPConstants[AP_MINIMUM] && + !gfHiddenInterrupt && + !gTacticalStatus.fInterruptOccurred && + !InARoom(pSoldier->sGridNo, NULL) && + !TileIsOutOfBounds(sClosestOpponent) && + pSoldier->aiData.bOrders != STATIONARY && + pSoldier->aiData.bOrders != ONGUARD && + !AICheckSpecialRole(pSoldier) && + (ubBestAttackAction == AI_ACTION_NONE || ubBestAttackAction == AI_ACTION_FIRE_GUN && BestAttack.ubChanceToReallyHit < 5 * RangeChangeDesire(pSoldier)) && + AIGunRange(pSoldier) < DAY_VISION_RANGE && + pSoldier->aiData.bAIMorale >= MORALE_NORMAL && + (!fAnyCover || !fProneSightCover || AIGunRange(pSoldier)*10 < distanceToOpponent) && + distanceToOpponent < 10*MAX_VISION_RANGE && + //DetermineMovementMode(pSoldier, AI_ACTION_GET_CLOSER) != CRAWLING && + pSoldier->aiData.bShock < RangeChangeDesire(pSoldier) * 2 && + (AIGunRange(pSoldier)*10 < distanceToOpponent || + pSoldier->aiData.bLastAttackHit && pSoldier->sLastTarget != NOWHERE || + pSoldier->aiData.bAIMorale == MORALE_FEARLESS || + ubBestAttackAction == AI_ACTION_NONE || + ubBestAttackAction == AI_ACTION_FIRE_GUN && BestAttack.ubChanceToReallyHit == 1 || + !fAnyCover)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Black cover advance]")); + + BOOLEAN fClimbDummy; + INT32 sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimbDummy); + + if (!TileIsOutOfBounds(sClosestDisturbance)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("closest reachable disturbance %d", sClosestDisturbance)); + + INT32 sAdvanceSpot = NOWHERE; + + DebugAI(AI_MSG_INFO, pSoldier, String("search for any cover advance spot")); + sAdvanceSpot = FindAdvanceSpot(pSoldier, sClosestDisturbance, AI_ACTION_GET_CLOSER, ADVANCE_SPOT_ANY_COVER, FALSE); + + if (!TileIsOutOfBounds(sAdvanceSpot)) + { + DebugAI(AI_MSG_INFO, pSoldier, String("found cover advance spot %d", sAdvanceSpot)); + + // check that we can reach desired location + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sAdvanceSpot, 0, AI_ACTION_GET_CLOSER, 0); + //if (pSoldier->aiData.usActionData == sAdvanceSpot) + if (pSoldier->aiData.usActionData != NOWHERE) + { + DebugAI(AI_MSG_INFO, pSoldier, String("cover advance spot ok")); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d status black advance to cover, target grid %d", pSoldier->ubID.i, sAdvanceSpot); + pSoldier->aiData.usActionData = sAdvanceSpot; + + //ScreenMsg(FONT_MCOLOR_LTGREEN, MSG_INTERFACE, L"[%d] found cover advance spot %d", pSoldier->ubID, sAdvanceSpot); + BeginMultiPurposeLocator(sAdvanceSpot, pSoldier->pathing.bLevel, FALSE); + + return AI_ACTION_GET_CLOSER; + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("cannot reach cover advance spot!")); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("cannot find cover advance spot")); + + // try to use smoke to cover advance movement + //gubNPCAPBudget = pSoldier->bActionPoints; + gubNPCAPBudget = 0; + gubNPCDistLimit = 0; + + // check path to closest disturbance + if (gfTurnBasedAI && + pSoldier->bActionPoints >= APBPConstants[AP_MINIMUM] && + !TileIsOutOfBounds(sClosestDisturbance) && + RangeChangeDesire(pSoldier) > 3 && + !AICheckIsSniper(pSoldier) && + !AICheckIsMachinegunner(pSoldier) && + pSoldier->aiData.bOrders != STATIONARY && + (pSoldier->aiData.bUnderFire || + pSoldier->aiData.bShock > 0 || + pSoldier->stats.bLife < pSoldier->stats.bLifeMax * 3 / 4 || + CountTeamUnderAttack(pSoldier->bTeam, pSoldier->sGridNo, DAY_VISION_RANGE / 2) > CountNearbyFriends(pSoldier, pSoldier->sGridNo, DAY_VISION_RANGE / 2) / 2 || + CountSeenEnemiesLastTurn(pSoldier) > CountNearbyFriends(pSoldier, pSoldier->sGridNo, DAY_VISION_RANGE / 2)) && + (Chance(SoldierDifficultyLevel(pSoldier) * 10) || Chance(TeamPercentKilled(pSoldier->bTeam)) || Chance(CountTeamUnderAttack(pSoldier->bTeam, pSoldier->sGridNo, DAY_VISION_RANGE / 2))) && + FindBestPath(pSoldier, sClosestDisturbance, pSoldier->pathing.bLevel, RUNNING, COPYROUTE, 0)) + { + INT16 sLoop; + INT32 sCoverSpot = NOWHERE; + + DebugAI(AI_MSG_INFO, pSoldier, String("found path to %d, path size %d ", sClosestDisturbance, pSoldier->pathing.usPathDataSize)); + + INT32 sCheckGridNo = pSoldier->sGridNo; + + for (sLoop = pSoldier->pathing.usPathIndex; sLoop < pSoldier->pathing.usPathDataSize; sLoop++) + { + sCheckGridNo = NewGridNo(sCheckGridNo, DirectionInc((UINT8)(pSoldier->pathing.usPathingData[sLoop]))); + + if (!TileIsOutOfBounds(sCheckGridNo) && + PythSpacesAway(pSoldier->sGridNo, sCheckGridNo) < TACTICAL_RANGE / 2 && + PythSpacesAway(pSoldier->sGridNo, sCheckGridNo) > TACTICAL_RANGE / 4 && + !Water(sCheckGridNo, pSoldier->pathing.bLevel) && + !InSmokeNearby(sCheckGridNo, pSoldier->pathing.bLevel) && + !InSmoke(sCheckGridNo, pSoldier->pathing.bLevel) && + (CorpseWarning(pSoldier, sCheckGridNo, pSoldier->pathing.bLevel) || InLightAtNight(sCheckGridNo, pSoldier->pathing.bLevel)) && + SightCoverAtSpot(pSoldier, sCheckGridNo, FALSE)) + { + CheckTossGrenadeAt(pSoldier, &BestThrow, sCheckGridNo, pSoldier->pathing.bLevel, EXPLOSV_SMOKE); + + if (BestThrow.ubPossible) + { + sCoverSpot = sCheckGridNo; + } + } + } + + if (!TileIsOutOfBounds(sCoverSpot)) + { + CheckTossGrenadeAt(pSoldier, &BestThrow, sCoverSpot, pSoldier->pathing.bLevel, EXPLOSV_SMOKE); + + if (BestThrow.ubPossible) + { + DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime)); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d status black advance to cover, throw smoke at target grid %d level %d aimtime %d", pSoldier->ubID.i, BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime); + + // if necessary, swap the usItem from holster into the hand position + if (BestThrow.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket")); + RearrangePocket(pSoldier, HANDPOS, BestThrow.bWeaponIn, FOREVER); + } + + // stand up before throwing if needed + if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestThrow.ubStance && + pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestThrow.sTarget), BestThrow.ubStance)) + { + pSoldier->aiData.usActionData = BestThrow.ubStance; + pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; + pSoldier->aiData.usNextActionData = BestThrow.sTarget; + pSoldier->aiData.bNextTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + return AI_ACTION_CHANGE_STANCE; + } + + pSoldier->aiData.usActionData = BestThrow.sTarget; + pSoldier->bTargetLevel = BestThrow.bTargetLevel; + pSoldier->aiData.bAimTime = BestThrow.ubAimTime; + + return(AI_ACTION_TOSS_PROJECTILE); + } + } + } + gubNPCAPBudget = 0; + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // POSSIBLY FORGET THE ATTACK AND TAKE COVER + //////////////////////////////////////////////////////////////////////////// + INT32 iCoverPercentBetter = 0; + INT32 sBestCover = NOWHERE; + BOOLEAN fAllowCoverCheck = FALSE; + if ((ubBestAttackAction == AI_ACTION_FIRE_GUN) && + (pSoldier->aiData.bShock > gGameExternalOptions.ubMaxSuppressionShock / 3) && + (pSoldier->stats.bLife >= pSoldier->stats.bLifeMax / 2) && + (BestAttack.ubChanceToReallyHit < 30) && + targetFarAway && + (RangeChangeDesire(pSoldier) >= 4) && + ubCanMove) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Allow taking cover]")); + // okay, really got to wonder about this... could taking cover be an option? + if (pSoldier->aiData.bOrders != STATIONARY && !gfHiddenInterrupt) + { + // make militia a bit more cautious + // 3 (UINT16) CONVERSIONS HERE TO AVOID ERRORS. GOTTHARD 7/15/08 + if (pSoldier->bTeam == MILITIA_TEAM && (INT16)(PreRandom(20)) > BestAttack.ubChanceToReallyHit || + pSoldier->bTeam != MILITIA_TEAM && (INT16)(PreRandom(40)) > BestAttack.ubChanceToReallyHit) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Allow cover check")); + // maybe taking cover would be better! + fAllowCoverCheck = TRUE; + + sBestCover = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iCoverPercentBetter); + if ((INT16)(PreRandom(10)) > BestAttack.ubChanceToReallyHit && + !TileIsOutOfBounds(sBestCover) && + (iCoverPercentBetter > 10 || !fAnyCover)) + { + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "DecideActionBlack: can't hit so screw the attack"); + DebugAI(AI_MSG_INFO, pSoldier, String("can't hit, screw the attack")); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d status black can't hit with chance %d, forget attack and allow cover check", pSoldier->ubID.i, BestAttack.ubChanceToReallyHit); + // screw the attack! + ubBestAttackAction = AI_ACTION_NONE; + } + } + } + } + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "LOOK FOR SOME KIND OF COVER BETTER THAN WHAT WE HAVE NOW"); + //////////////////////////////////////////////////////////////////////////// + // LOOK FOR SOME KIND OF COVER BETTER THAN WHAT WE HAVE NOW + //////////////////////////////////////////////////////////////////////////// + + // if soldier has enough APs left to move at least 1 square's worth, + // and either he can't attack any more or is in a dangerous spot + if ((ubCanMove && !SkipCoverCheck && !gfHiddenInterrupt && ubBestAttackAction == AI_ACTION_NONE) || + fAllowCoverCheck || Chance(currentHealthSituation(pSoldier->stats.bLife, pSoldier->stats.bLifeMax))) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Find cover]")); + // sevenfm: if not found yet + if (TileIsOutOfBounds(sBestCover)) + { + sBestCover = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iCoverPercentBetter, NOWHERE, true); //Ignore search radius limit for testing + } + if ( sBestCover != NOWHERE ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Found cover spot %d percent better %d movement mode %d", sBestCover, iCoverPercentBetter, DetermineMovementMode(pSoldier, AI_ACTION_TAKE_COVER))); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d Found cover spot %d percent better %d movement mode %d", pSoldier->ubID.i, sBestCover, iCoverPercentBetter, DetermineMovementMode(pSoldier, AI_ACTION_TAKE_COVER)); + } + } + + + + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "DecideActionBlack: DECIDE BETWEEN ATTACKING AND DEFENDING (TAKING COVER)"); + ////////////////////////////////////////////////////////////////////////// + // IF NECESSARY, DECIDE BETWEEN ATTACKING AND DEFENDING (TAKING COVER) + ////////////////////////////////////////////////////////////////////////// + + // if both are possible + if ((ubBestAttackAction != AI_ACTION_NONE) && (!TileIsOutOfBounds(sBestCover))) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide attack/cover]")); + // gotta compare their merits and select the more desirable option + INT32 iOffense = BestAttack.ubChanceToReallyHit; + INT32 iDefense = iCoverPercentBetter; + + // based on how we feel about the situation, decide whether to attack first + switch (pSoldier->aiData.bAIMorale) + { + case MORALE_FEARLESS: + iOffense += iOffense / 2; // increase 50% + break; + + case MORALE_CONFIDENT: + iOffense += iOffense / 4; // increase 25% + break; + + case MORALE_NORMAL: + break; + + case MORALE_WORRIED: + iDefense += iDefense / 4; // increase 25% + break; + + case MORALE_HOPELESS: + iDefense += iDefense / 2; // increase 50% + break; + } + + + // smart guys more likely to try to stay alive, dolts more likely to shoot! + if (pSoldier->stats.bWisdom >= 50) //Madd: reduced the wisdom required to want to live... + iDefense += 10; + else if (pSoldier->stats.bWisdom < 30) + iDefense -= 10; + + // some orders are more offensive, others more defensive + if (pSoldier->aiData.bOrders == SEEKENEMY) + iOffense += 10; + else if ((pSoldier->aiData.bOrders == STATIONARY) || (pSoldier->aiData.bOrders == ONGUARD) || pSoldier->aiData.bOrders == SNIPER) + iDefense += 10; + + switch (pSoldier->aiData.bAttitude) + { + case DEFENSIVE: iDefense += 30; break; + case BRAVESOLO: iDefense -= 0; break; + case BRAVEAID: iDefense -= 0; break; + case CUNNINGSOLO: iDefense += 20; break; + case CUNNINGAID: iDefense += 20; break; + case AGGRESSIVE: iOffense += 10; break; + case ATTACKSLAYONLY: iOffense += 30; break; + } + + DebugAI(AI_MSG_INFO, pSoldier, String("iOffense %d iDefense %d", iOffense, iDefense)); + // if his defensive instincts win out, forget all about the attack + if (iDefense > iOffense) + { + DebugAI(AI_MSG_INFO, pSoldier, String("[decided taking cover, disable attack]")); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d Decided taking cover, iOffense %d iDefense %d", pSoldier->ubID.i, iOffense, iDefense); + ubBestAttackAction = AI_ACTION_NONE; + } + } + + + ////////////////////////////////////////////////////////////////////////// + // PREPARE ATTACK + ////////////////////////////////////////////////////////////////////////// + DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("DecideActionBlack: is attack still desirable? ubBestAttackAction = %d", ubBestAttackAction)); + + // if attack is still desirable (meaning it's also preferred to taking cover) + if (ubBestAttackAction != AI_ACTION_NONE) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Prepare attack]")); + // if we wanted to be REALLY mean, we could look at chance to hit and decide whether + // to shoot at the head... + + // default settings + //POSSIBLE STRUCTURE CHANGE PROBLEM, NOT CURRENTLY CHANGED. GOTTHARD 7/14/08 + pSoldier->aiData.bAimTime = BestAttack.ubAimTime; + pSoldier->bScopeMode = BestAttack.bScopeMode; + pSoldier->bDoBurst = 0; + + // HEADROCK HAM 3.6: bAimTime represents how MANY aiming levels are used, not how much APs they cost necessarily. + INT16 sActualAimAP = CalcAPCostForAiming(pSoldier, BestAttack.sTarget, (INT8)pSoldier->aiData.bAimTime); + + if (ubBestAttackAction == AI_ACTION_FIRE_GUN) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Prepare shooting]")); + + auto& weapon = pSoldier->inv[BestAttack.bWeaponIn]; + const auto shotsLeft = weapon[0]->data.gun.ubGunShotsLeft; + const bool noSemiAuto = Weapon[weapon.usItem].NoSemiAuto; + + ////////////////////////////////////////////////////////////////////////// + // IF ENOUGH APs TO BURST, RANDOM CHANCE OF DOING SO + ////////////////////////////////////////////////////////////////////////// + + if ( BestAttack.ubOpponent->stats.bLife >= OKLIFE ) // Don't burst at downed targets ) + { + if ( IsGunBurstCapable(&weapon, FALSE, pSoldier) && shotsLeft > 1 ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("enough APs to burst, random chance of doing so")); + INT16 ubBurstAPs = CalcAPsToBurst(pSoldier->CalcActionPoints(), &weapon, pSoldier); + + // HEADROCK HAM 3.6: Use Actual Aiming Time. + if ( pSoldier->bActionPoints >= BestAttack.ubAPCost + sActualAimAP + ubBurstAPs ) + { + // Base chance of bursting is 25% if best shot was +0 aim, down to 8% at +4 + INT32 iChance = (25 / max((BestAttack.ubAimTime + 1), 1)); + switch ( pSoldier->aiData.bAttitude ) + { + case DEFENSIVE: iChance += -5; break; + case BRAVESOLO: iChance += 5; break; + case BRAVEAID: iChance += 5; break; + case CUNNINGSOLO: iChance += 0; break; + case CUNNINGAID: iChance += 0; break; + case AGGRESSIVE: iChance += 10; break; + case ATTACKSLAYONLY:iChance += 30; break; + } + + // SANDRO: more likely to burst when firing from hip + if ( BestAttack.bScopeMode == USE_ALT_WEAPON_HOLD && ItemIsTwoHanded(weapon.usItem) ) + iChance += 40; + + // CHRISL: Changed from a simple flag to two externalized values for more modder control over AI suppression + if ( GetMagSize(&weapon, 0) >= gGameExternalOptions.ubAISuppressionMinimumMagSize && + shotsLeft >= gGameExternalOptions.ubAISuppressionMinimumAmmo ) + iChance += 20; + + // increase chance based on proximity and difficulty of enemy + if ( PythSpacesAway(pSoldier->sGridNo, BestAttack.sTarget) < 15 ) + { + DebugMsg(TOPIC_JA2AI, DBG_LEVEL_3, String("DecideActionBlack: check chance to burst")); + iChance += (15 - PythSpacesAway(pSoldier->sGridNo, BestAttack.sTarget)) * (1 + SoldierDifficultyLevel(pSoldier)); + if ( pSoldier->aiData.bAttitude == ATTACKSLAYONLY ) + { + // increase it more! + iChance += 5 * (15 - PythSpacesAway(pSoldier->sGridNo, BestAttack.sTarget)); + } + } + + // Increase burst chance based on difficulty level if target is close + if ( PythSpacesAway(pSoldier->sGridNo, BestAttack.sTarget) < 10 ) + { + iChance += 25 * gGameOptions.ubDifficultyLevel; + } + + if ( (INT32)PreRandom(100) < iChance ) + { + BestAttack.ubAPCost += ubBurstAPs + sActualAimAP;//dnl ch58 130913 + // check for spread burst possibilities + if ( pSoldier->aiData.bAttitude != ATTACKSLAYONLY ) + { + CalcSpreadBurst(pSoldier, BestAttack.sTarget, BestAttack.bTargetLevel); + } + //dnl ch58 130913 return aiming for burst + pSoldier->bDoBurst = 1; + pSoldier->bDoAutofire = 0; + } + } + } + + if ( IsGunAutofireCapable(&weapon) && (pSoldier->bDoBurst == 0 || noSemiAuto) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("enough APs to autofire, random chance of doing so")); + INT16 ubBurstAPs; + L_NEWAIM: + INT16 totalAPs = 0; + FLOAT dTotalRecoil = 0.0f; + pSoldier->bDoAutofire = 0; + if ( UsingNewCTHSystem() ) + { + do + { + pSoldier->bDoAutofire++; + dTotalRecoil += AICalcRecoilForShot(pSoldier, &weapon, pSoldier->bDoAutofire); + ubBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &weapon, pSoldier->bDoAutofire, pSoldier); + totalAPs = BestAttack.ubAPCost + ubBurstAPs + sActualAimAP; + } while ( pSoldier->bActionPoints >= totalAPs && shotsLeft >= pSoldier->bDoAutofire && dTotalRecoil <= 10.0f ); + } + else + { + do + { + pSoldier->bDoAutofire++; + ubBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &(weapon), pSoldier->bDoAutofire, pSoldier); + totalAPs = BestAttack.ubAPCost + ubBurstAPs + sActualAimAP; + } while ( pSoldier->bActionPoints >= totalAPs && shotsLeft >= pSoldier->bDoAutofire && GetAutoPenalty(&weapon, gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_PRONE) * pSoldier->bDoAutofire <= 80 ); + } + pSoldier->bDoAutofire--; + + DebugAI(AI_MSG_INFO, pSoldier, String("autofire %d", pSoldier->bDoAutofire)); + + //dnl ch69 130913 let try increase autofire rate for aim cost + // sevenfm: LIMIT_MAX_DEVIATION option increases effectiveness of suppression + if ( (!UsingNewCTHSystem() || gGameCTHConstants.LIMIT_MAX_DEVIATION) && + pSoldier->bDoAutofire < 3 && + pSoldier->aiData.bAimTime > 0 && + shotsLeft >= 3 && + Chance(gGameExternalOptions.sSuppressionEffectiveness) && + (!gGameExternalOptions.fAISafeSuppression || CheckSuppressionDirection(pSoldier, BestAttack.sTarget, BestAttack.bTargetLevel)) ) + { + if ( pSoldier->aiData.bAimTime > 0 ) { pSoldier->aiData.bAimTime--; } + + sActualAimAP = CalcAPCostForAiming(pSoldier, BestAttack.sTarget, (INT8)pSoldier->aiData.bAimTime); + DebugAI(AI_MSG_INFO, pSoldier, String("reduce aim to %d, recalc autofire, aim AP %d", pSoldier->aiData.bAimTime, sActualAimAP)); + goto L_NEWAIM; + } + + if ( pSoldier->bDoAutofire > 0 ) + { + ubBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &weapon, pSoldier->bDoAutofire, pSoldier); + + if ( pSoldier->bActionPoints >= BestAttack.ubAPCost + sActualAimAP + ubBurstAPs ) + { + // Base chance of bursting is 25% if best shot was +0 aim, down to 8% at +4 + INT32 iChance = (100 / max((BestAttack.ubAimTime + 1), 1)); + switch ( pSoldier->aiData.bAttitude ) + { + case DEFENSIVE: iChance += -5; break; + case BRAVESOLO: iChance += 5; break; + case BRAVEAID: iChance += 5; break; + case CUNNINGSOLO: iChance += 0; break; + case CUNNINGAID: iChance += 0; break; + case AGGRESSIVE: iChance += 10; break; + case ATTACKSLAYONLY:iChance += 30; break; + } + + // SANDRO: more likely to burst when firing from hip + if ( BestAttack.bScopeMode == USE_ALT_WEAPON_HOLD && ItemIsTwoHanded(weapon.usItem) ) + iChance += 40; + + // CHRISL: Changed from a simple flag to two externalized values for more modder control over AI suppression + if ( GetMagSize(&weapon, 0) >= gGameExternalOptions.ubAISuppressionMinimumMagSize && shotsLeft >= gGameExternalOptions.ubAISuppressionMinimumAmmo ) + iChance += 30; + + if ( bInGas ) + iChance += 50; //Madd: extra chance of going nuts and autofiring if stuck in gas + + // increase chance based on proximity and difficulty of enemy + if ( PythSpacesAway(pSoldier->sGridNo, BestAttack.sTarget) < 15 ) + { + DebugMsg(TOPIC_JA2AI, DBG_LEVEL_3, String("DecideActionBlack: check chance to autofire")); + iChance += (15 - PythSpacesAway(pSoldier->sGridNo, BestAttack.sTarget)) * (1 + SoldierDifficultyLevel(pSoldier)); + if ( pSoldier->aiData.bAttitude == ATTACKSLAYONLY ) + { + // increase it more! + iChance += 5 * (15 - PythSpacesAway(pSoldier->sGridNo, BestAttack.sTarget)); + } + } + // HEADROCK HAM 3.6: Forcing enemies to autofire at close range if possible, similar to forced burst (see above) + //if ( PythSpacesAway(pSoldier->sGridNo, BestAttack.sTarget) < 10 && gGameOptions.ubDifficultyLevel > DIF_LEVEL_EASY ) + //{ + // iChance += 100; + //} + + DebugAI(AI_MSG_INFO, pSoldier, String("chance for autofire %d", iChance)); + + if ( (INT32)PreRandom(100) < iChance || noSemiAuto ) + { + //dnl ch69 140913 return aiming for autofire with halfautofire fix + pSoldier->bDoBurst = 1; + INT16 ubHalfBurstAPs = 256; + if ( shotsLeft < 4 ) + { + iChance = 0; + } + else + { + ubHalfBurstAPs = CalcAPsToAutofire(pSoldier->CalcActionPoints(), &weapon, 2, pSoldier); + + if ( !CheckSuppressionDirection(pSoldier, BestAttack.sTarget, BestAttack.bTargetLevel) ) + iChance = 100; + else + iChance = BestAttack.ubChanceToReallyHit / 2; + + if ( noSemiAuto || pSoldier->aiData.bOppCnt > 1 ) + iChance += (100 - iChance) / 2; + } + + if ( Chance(iChance) && pSoldier->bActionPoints >= (2 * BestAttack.ubAPCost + ubHalfBurstAPs + sActualAimAP) ) + { + // Try short autofire to enhance chance of hitting + pSoldier->bDoAutofire = 2; + BestAttack.ubAPCost += ubHalfBurstAPs + sActualAimAP; + } + else + { + BestAttack.ubAPCost += ubBurstAPs + sActualAimAP; + } + } + else + { + pSoldier->bDoAutofire = 0; + pSoldier->bDoBurst = 0; + } + } + } + } + } + + if (!pSoldier->bDoBurst) + { + pSoldier->aiData.bAimTime = BestAttack.ubAimTime; + pSoldier->bDoBurst = 0; + pSoldier->bDoAutofire = 0; + } + + ////////////////////////////////////////////////////////////////////////// + // IF WAY OUT OF EFFECTIVE RANGE TRY TO ADVANCE RESERVING ENOUGH AP FOR A SHOT IF NOT ACTED YET + ////////////////////////////////////////////////////////////////////////// + if ((pSoldier->bActionPoints > BestAttack.ubAPCost) && + (pSoldier->aiData.bShock < gGameExternalOptions.ubMaxSuppressionShock / 3) && + (pSoldier->stats.bLife >= pSoldier->stats.bLifeMax / 2) && + (BestAttack.ubChanceToReallyHit < 8) && + targetFarAway && + (RangeChangeDesire(pSoldier) >= 3)) // Cunning and above + { + //sClosestOpponent = BestAttack.ubOpponent->sGridNo; + DebugAI(AI_MSG_INFO, pSoldier, String("check if can advance to closest opponent %d", BestAttack.ubOpponent->sGridNo)); + + decision = MoveCloserBeforeShooting(pSoldier, BestAttack); + if ( decision != AI_ACTION_INVALID ) + { + return decision; + } + } + } + else if (ubBestAttackAction == AI_ACTION_THROW_KNIFE) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Prepare throwing knife]")); + + if (BestAttack.bWeaponIn != HANDPOS && gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_STAND) + { + // we had better make sure we lower our gun first! + pSoldier->aiData.bAction = AI_ACTION_LOWER_GUN; + pSoldier->aiData.usActionData = 0; + + // queue up attack for after we lower weapon if any + pSoldier->aiData.bNextAction = AI_ACTION_THROW_KNIFE; + pSoldier->aiData.usNextActionData = BestAttack.sTarget; + pSoldier->aiData.bNextTargetLevel = BestAttack.bTargetLevel; + } + + } + // SANDRO - chance to make aimed punch/stab for martial arts/melee + else if (ubBestAttackAction == AI_ACTION_KNIFE_MOVE && gGameOptions.fNewTraitSystem) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Prepare knife attack]")); + + pSoldier->aiData.bAimTime = 0; + INT32 iChance = 0; + + if (Item[pSoldier->inv[BestAttack.bWeaponIn].usItem].usItemClass == IC_PUNCH) + { + if (gGameExternalOptions.fEnhancedCloseCombatSystem) + iChance += 30; + if (HAS_SKILL_TRAIT(pSoldier, MARTIAL_ARTS_NT)) + iChance += 30 * NUM_SKILL_TRAITS(pSoldier, MARTIAL_ARTS_NT); + + if ((INT32)PreRandom(100) <= iChance) + { + pSoldier->aiData.bAimTime = (gGameExternalOptions.fEnhancedCloseCombatSystem ? gSkillTraitValues.ubModifierForAPsAddedOnAimedPunches : 6); + } + } + else + { + if (gGameExternalOptions.fEnhancedCloseCombatSystem) + iChance += 30; + if (HAS_SKILL_TRAIT(pSoldier, MELEE_NT)) + iChance += 30; + + if ((INT32)PreRandom(100) <= iChance) + { + pSoldier->aiData.bAimTime = (gGameExternalOptions.fEnhancedCloseCombatSystem ? gSkillTraitValues.ubModifierForAPsAddedOnAimedBladedAttackes : 6); + } + } + } + + ////////////////////////////////////////////////////////////////////////// + // POSSIBLY MOVE CLOSER TO PINNED DOWN ENEMIES + ////////////////////////////////////////////////////////////////////////// + if (ubBestAttackAction == AI_ACTION_FIRE_GUN) + { + SOLDIERTYPE* opponent = BestAttack.ubOpponent; + + if ((pSoldier->bActionPoints > BestAttack.ubAPCost) && + (pSoldier->aiData.bShock < gGameExternalOptions.ubMaxSuppressionShock / 3) && + (pSoldier->stats.bLife >= pSoldier->stats.bLifeMax / 2) && + (RangeChangeDesire(pSoldier) >= 2) && + opponent->IsCowering() && pSoldier->aiData.bOppCnt < CountNearbyFriends(pSoldier, pSoldier->sGridNo, 10)) + { + //sClosestOpponent = opponent->sGridNo; + DebugAI(AI_MSG_INFO, pSoldier, String("check if can advance to pinned down opponent %d", opponent->sGridNo)); + + decision = MoveCloserBeforeShooting(pSoldier, BestAttack); + if ( decision != AI_ACTION_INVALID ) + { + return decision; + } + } + } + + + ////////////////////////////////////////////////////////////////////////// + // OTHERWISE, JUST GO AHEAD & ATTACK! + ////////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_TOPIC, pSoldier, String("Attack!")); + + //dnl ch64 270813 must be as below RearrangePocket with FOREVER will screw already decided BURST or AUTOFIRE + INT8 bDoBurst = pSoldier->bDoBurst; + UINT8 bDoAutofire = pSoldier->bDoAutofire; + // swap weapon to hand if necessary + if (BestAttack.bWeaponIn != HANDPOS) + { + DebugAI(AI_MSG_INFO, pSoldier, String("swap weapon into hand from %d", BestAttack.bWeaponIn)); + RearrangePocket(pSoldier, HANDPOS, BestAttack.bWeaponIn, FOREVER); + } + + if (ubBestAttackAction == AI_ACTION_FIRE_GUN && bDoBurst == 1)//dnl ch64 270813 + { + DebugAI(AI_MSG_INFO, pSoldier, String("using burst/autofire")); + + pSoldier->bDoAutofire = bDoAutofire; + pSoldier->bDoBurst = bDoBurst; + if (bDoAutofire > 1) + pSoldier->bWeaponMode = WM_AUTOFIRE; + else + pSoldier->bWeaponMode = WM_BURST; + } + + DebugAI(AI_MSG_INFO, pSoldier, String("prepare attack at target %d level %d aim %d ap %d cth %d opponent %d", BestAttack.sTarget, BestAttack.bTargetLevel, BestAttack.ubAimTime, BestAttack.ubAPCost, BestAttack.ubChanceToReallyHit, BestAttack.ubOpponent)); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d prepare attack at target %d level %d aim %d ap %d cth %d opponent %d", pSoldier->ubID.i, BestAttack.sTarget, BestAttack.bTargetLevel, BestAttack.ubAimTime, BestAttack.ubAPCost, BestAttack.ubChanceToReallyHit, BestAttack.ubOpponent); + + if (ubBestAttackAction == AI_ACTION_FIRE_GUN) + { + if (gAnimControl[pSoldier->usAnimState].ubEndHeight != BestAttack.ubStance && + IsValidStance(pSoldier, BestAttack.ubStance)) + { + pSoldier->aiData.bNextAction = AI_ACTION_FIRE_GUN; + pSoldier->aiData.usNextActionData = BestAttack.sTarget; + pSoldier->aiData.bNextTargetLevel = BestAttack.bTargetLevel; + pSoldier->aiData.usActionData = BestAttack.ubStance; + + DebugAI(AI_MSG_INFO, pSoldier, String("Change stance before shooting")); + return(AI_ACTION_CHANGE_STANCE); + } + else + { + pSoldier->aiData.usActionData = BestAttack.sTarget; + pSoldier->bTargetLevel = BestAttack.bTargetLevel; + DebugAI(AI_MSG_INFO, pSoldier, String("Fire weapon!")); + return(AI_ACTION_FIRE_GUN); + } + } + else if (ubBestAttackAction == AI_ACTION_TOSS_PROJECTILE) + { + DebugAI(AI_MSG_INFO, pSoldier, String("toss attack, disable burst/autofire")); + pSoldier->bDoBurst = 0; + pSoldier->bDoAutofire = 0; + + if (IsGrenadeLauncherAttached(&pSoldier->inv[HANDPOS])) //dnl ch63 240813 + { + DebugAI(AI_MSG_INFO, pSoldier, String("using attached GL")); + pSoldier->bWeaponMode = WM_ATTACHED_GL; + } + + // stand up before throwing if needed + if (gAnimControl[pSoldier->usAnimState].ubEndHeight < BestAttack.ubStance && + pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, BestAttack.sTarget), BestAttack.ubStance)) + { + pSoldier->aiData.usActionData = BestAttack.ubStance; + pSoldier->aiData.bNextAction = AI_ACTION_TOSS_PROJECTILE; + pSoldier->aiData.usNextActionData = BestAttack.sTarget; + pSoldier->aiData.bNextTargetLevel = BestAttack.bTargetLevel; + return AI_ACTION_CHANGE_STANCE; + } + else + { + pSoldier->aiData.usActionData = BestAttack.sTarget; + pSoldier->bTargetLevel = BestAttack.bTargetLevel; + return(AI_ACTION_TOSS_PROJECTILE); + } + } + // other attacks + else + { + pSoldier->aiData.usActionData = BestAttack.sTarget; + pSoldier->bTargetLevel = BestAttack.bTargetLevel; + return(ubBestAttackAction); + } + } + + + ////////////////////////////////////////////////////////////////////// + // CLIMB ROOF / JUMP THROUGH WINDOW + ////////////////////////////////////////////////////////////////////// + // get the location of the closest reachable opponent + /* Flugente 22.02.2012 - A few clarifications: I changed ClosestSeenOpponent so that for zombies, this function also returns an opponent if he is on the + * roof of a building, we are not, but our GridNo belongs to that same building. + * If that is the case, it is clear that we have to get on that roof. However, we cannot do that in BlackState. If, by pure chance, we can still see our + * enemy, we cannot climb (there is no climbing option in BlackState sofar). + * So, I changed the code so that now we will climb the roof. + */ + INT32 targetGridNo = -1; + INT8 targetbLevel = 0; + sClosestOpponent = ClosestSeenOpponentWithRoof(pSoldier, &targetGridNo, &targetbLevel); + if (!TileIsOutOfBounds(sClosestOpponent) && !TileIsOutOfBounds(targetGridNo) && SameBuilding(pSoldier->sGridNo, targetGridNo)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Window jump]")); + if (targetbLevel == pSoldier->pathing.bLevel && targetbLevel == 0) + { + ////////////////////////////////////////////////////////////////////// + // GO DIRECTLY TOWARDS CLOSEST KNOWN OPPONENT + ////////////////////////////////////////////////////////////////////// + + // try to move towards him + pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier, sClosestOpponent, AI_ACTION_GET_CLOSER); + + // Flugente: if on the same level and there is a jumpable window here, jump through it + if (gGameExternalOptions.fCanJumpThroughWindows) + { + // determine if there is a jumpable window in the direction to our target + // if yes, and we are not facing it, face it now + // if yes, and we are facing it, jump + // if no, go on, nothing to see here + // determine direction of our target + INT8 targetdirection = (INT8)GetDirectionToGridNoFromGridNo(pSoldier->sGridNo, sClosestOpponent); + + // determine if there is a jumpable window here, in the direction of our target + // store old direction for this check + UINT8 tmpdirection = pSoldier->ubDirection; + pSoldier->ubDirection = targetdirection; + + INT8 windowdirection = DIRECTION_IRRELEVANT; + if (FindWindowJumpDirection(pSoldier, pSoldier->sGridNo, pSoldier->ubDirection, &windowdirection) && targetdirection == windowdirection) + { + pSoldier->ubDirection = tmpdirection; + + // are we already looking in that direction? + if (pSoldier->ubDirection == targetdirection) + { + // jump through the window + return(AI_ACTION_JUMP_WINDOW); + } + else + { + // look into that direction + if (pSoldier->InternalIsValidStance(targetdirection, gAnimControl[pSoldier->usAnimState].ubEndHeight)) + { + pSoldier->aiData.usActionData = targetdirection; + return(AI_ACTION_CHANGE_FACING); + } + + } + } + + pSoldier->ubDirection = tmpdirection; + } + } + // The situation mentioned above happens... + else + { + // need to climb AND have enough APs to get there this turn + BOOLEAN fUp = TRUE; + if (pSoldier->pathing.bLevel > 0) + fUp = FALSE; + + if ((pSoldier->bActionPoints > GetAPsToClimbRoof(pSoldier, fUp))) + { + pSoldier->aiData.usActionData = targetGridNo;//FindClosestClimbPoint(pSoldier, fUp ); + + // Necessary test: can we climb up at this position? It might happen that our target is directly above us, then we'll have to move + INT8 newdirection; + if ((fUp && FindHeigherLevel(pSoldier, pSoldier->sGridNo, pSoldier->ubDirection, &newdirection)) || (!fUp && FindLowerLevel(pSoldier, pSoldier->sGridNo, pSoldier->ubDirection, &newdirection))) + { + return(AI_ACTION_CLIMB_ROOF); + } + else + { + return(AI_ACTION_SEEK_OPPONENT); + } + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // IF A LOCATION WITH BETTER COVER IS AVAILABLE & REACHABLE, GO FOR IT! + //////////////////////////////////////////////////////////////////////////// + if (!TileIsOutOfBounds(sBestCover)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Take cover]")); +#ifdef DEBUGDECISIONS + STR tempstr = ""; + sprintf(tempstr, "%s - TAKING COVER at gridno %d (%d%% better)\n", + pSoldier->name, sBestCover, iCoverPercentBetter); + DebugAI(tempstr); +#endif + ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d taking cover, morale %d, from %d to %d", pSoldier->ubID.i, pSoldier->aiData.bAIMorale, pSoldier->sGridNo, sBestCover ); + pSoldier->aiData.usActionData = sBestCover; + if (!TileIsOutOfBounds(sClosestOpponent))//dnl ch58 150913 After taking cover change facing toward recent target or closest enemy, currently such turn not charge APs and seems because AI is still in moving animation from take cover action + { + if (!TileIsOutOfBounds(pSoldier->sLastTarget)) + sClosestOpponent = pSoldier->sLastTarget; + pSoldier->aiData.bNextAction = AI_ACTION_CHANGE_FACING; + pSoldier->aiData.usNextActionData = GetDirectionFromCenterCellXYGridNo(sBestCover, sClosestOpponent); + } + return(AI_ACTION_TAKE_COVER); + } + + + //////////////////////////////////////////////////////////////////////////// + // IF THINGS ARE REALLY HOPELESS, OR UNARMED, TRY TO RUN AWAY + //////////////////////////////////////////////////////////////////////////// + + // if soldier has enough APs left to move at least 1 square's worth + if (ubCanMove) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Run away]")); + if ((pSoldier->aiData.bAIMorale == MORALE_HOPELESS) || !bCanAttack) + { + // look for best place to RUN AWAY to (farthest from the closest threat) + //pSoldier->aiData.usActionData = RunAway( pSoldier ); + pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("RUNNING AWAY to grid %d", pSoldier->aiData.usActionData)); + return(AI_ACTION_RUN_AWAY); + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // IF SPOTTERS HAVE BEEN CALLED FOR, AND WE HAVE SOME NEW SIGHTINGS, RADIO! + //////////////////////////////////////////////////////////////////////////// + + // if we're a computer merc, and we have the action points remaining to RADIO + // (we never want NPCs to choose to radio if they would have to wait a turn) + // and we're not swimming in deep water, and somebody has called for spotters + // and we see the location of at least 2 opponents + if (!(pSoldier->usSoldierFlagMask & SOLDIER_RAISED_REDALERT) && (gTacticalStatus.ubSpottersCalledForBy != NOBODY) && (pSoldier->bActionPoints >= APBPConstants[AP_RADIO]) && + (pSoldier->aiData.bOppCnt > 1) && + (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1) && !bInDeepWater) + { + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Radio sightings]")); + // base chance depends on how much new info we have to radio to the others + INT32 iChance = 25 * WhatIKnowThatPublicDont(pSoldier, TRUE); // just count them + + // if I actually know something they don't + if (iChance) + { + if ((INT16)PreRandom(100) < iChance) + { + return(AI_ACTION_RED_ALERT); + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // CROUCH IF NOT CROUCHING ALREADY + //////////////////////////////////////////////////////////////////////////// + decision = DecideActionChangeStance(pSoldier, ubCanMove, BestAttack, ubBestAttackAction, gLogDecideActionBlack); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + + + //////////////////////////////////////////////////////////////////////////// + // TURN TO FACE CLOSEST KNOWN OPPONENT (IF NOT FACING THERE ALREADY) + //////////////////////////////////////////////////////////////////////////// + decision = DecideActionChangeFacing(pSoldier, ubCanMove, gLogDecideActionBlack); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + + + //////////////////////////////////////////////////////////////////////////// + // RADIO RED ALERT: determine %chance to call others and report contact + //////////////////////////////////////////////////////////////////////////// + // if a militia has absofreaking nothing else to do, maybe they should radio in a report! + //if (!bInDeepWater && pSoldier->bTeam == MILITIA_TEAM) + if (!bInDeepWater) + { + decision = DecideActionRadioRedAlert(pSoldier, gLogDecideActionBlack); + if (decision != AI_ACTION_INVALID) + { + return decision; + } + } + + + //////////////////////////////////////////////////////////////////////////// + // LEAVE THE SECTOR + //////////////////////////////////////////////////////////////////////////// + + // NOT IMPLEMENTED + + //////////////////////////////////////////////////////////////////////////// + // DO NOTHING: Not enough points left to move, so save them for next turn + //////////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Nothing to do]")); + + // by default, if everything else fails, just stand in place and wait + pSoldier->aiData.usActionData = NOWHERE; + return(AI_ACTION_NONE); +} + +INT8 DecideActionBlackSoldierUtilityAI(SOLDIERTYPE* pSoldier) +{ + // if we have absolutely no action points, we can't do a thing under BLACK! + if ( pSoldier->bActionPoints <= 0 ) + { + pSoldier->aiData.usActionData = NOWHERE; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_NONE); + } + + + // Can this guy move to any of the neighbouring squares ? (sets TRUE/FALSE) + UINT8 ubCanMove = (pSoldier->bActionPoints >= MinPtsToMove(pSoldier)); + if ( pSoldier->flags.uiStatusFlags & (SOLDIER_DRIVER | SOLDIER_PASSENGER) ) + { + ubCanMove = 0; + } + + // Before deciding anything, stop cowering + if ( ubCanMove && + pSoldier->stats.bLife >= OKLIFE && + !pSoldier->bCollapsed && + !pSoldier->bBreathCollapsed && + pSoldier->IsCowering() ) + { + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d stop cowering", pSoldier->ubID.i); + return AI_ACTION_STOP_COWERING; + } + + + + return(AI_ACTION_NONE); } diff --git a/TacticalAI/PanicButtons.cpp b/TacticalAI/PanicButtons.cpp index 679e5e65f..e58ea4a9c 100644 --- a/TacticalAI/PanicButtons.cpp +++ b/TacticalAI/PanicButtons.cpp @@ -244,7 +244,7 @@ void PossiblyMakeThisEnemyChosenOne( SOLDIERTYPE * pSoldier ) } -INT8 PanicAI(SOLDIERTYPE *pSoldier, UINT8 ubCanMove) +ActionType PanicAI(SOLDIERTYPE *pSoldier, UINT8 ubCanMove) { BOOLEAN fFoundRoute = FALSE; INT8 bSlot; @@ -307,7 +307,7 @@ INT8 PanicAI(SOLDIERTYPE *pSoldier, UINT8 ubCanMove) if (bPanicTrigger == -1) { // augh! - return( -1 ); + return( AI_ACTION_INVALID ); } sPanicTriggerGridNo = gTacticalStatus.sPanicTriggerGridNo[ bPanicTrigger ]; @@ -430,7 +430,7 @@ INT8 PanicAI(SOLDIERTYPE *pSoldier, UINT8 ubCanMove) } // no action decided - return(-1); + return(AI_ACTION_INVALID); } void InitPanicSystem( void ) @@ -534,7 +534,7 @@ BOOLEAN NeedToRadioAboutPanicTrigger( void ) #define STAIRCASE_GRIDNO 12067 #define STAIRCASE_DIRECTION 0 -INT8 HeadForTheStairCase( SOLDIERTYPE * pSoldier ) +ActionType HeadForTheStairCase( SOLDIERTYPE * pSoldier ) { UNDERGROUND_SECTORINFO * pBasementInfo; diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index 9fd8a10e7..3864cde7b 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -106,7 +106,7 @@ typedef enum AI_ACTION_DOCTOR_SELF, // added by Flugente: AI-ONLY! bandage/surgery on self. DO NOT USE THIS FOR MERCS!!! AI_ACTION_SELFDETONATE, // added by Flugente: blow up an explosive in own inventory AI_ACTION_STOP_MEDIC, // sevenfm: stop giving aid animation - AI_ACTION_LAST = AI_ACTION_STOP_MEDIC + AI_ACTION_INVALID } ActionType; @@ -181,11 +181,26 @@ enum { AI_MSG_START, AI_MSG_DECIDE, AI_MSG_INFO, AI_MSG_TOPIC }; void DebugAI(INT8 bMsgType, SOLDIERTYPE *pSoldier, STR szOutput, bool doLog = true, INT8 bAction = -1); void DebugQuestInfo(STR szOutput); INT8 DecideAction(SOLDIERTYPE *pSoldier); -INT8 DecideActionBlack(SOLDIERTYPE *pSoldier); -INT8 DecideActionEscort(SOLDIERTYPE *pSoldier); INT8 DecideActionGreen(SOLDIERTYPE *pSoldier); -INT8 DecideActionRed(SOLDIERTYPE *pSoldier); INT8 DecideActionYellow(SOLDIERTYPE *pSoldier); +INT8 DecideActionRed(SOLDIERTYPE *pSoldier); +INT8 DecideActionBlack(SOLDIERTYPE *pSoldier); +INT8 DecideActionGreenBoxer(SOLDIERTYPE* pSoldier); +INT8 DecideActionBlackBoxer(SOLDIERTYPE* pSoldier); +INT8 DecideActionGreenCivilian(SOLDIERTYPE* pSoldier); +INT8 DecideActionYellowCivilian(SOLDIERTYPE* pSoldier); +INT8 DecideActionRedCivilian(SOLDIERTYPE* pSoldier); +INT8 DecideActionBlackCivilian(SOLDIERTYPE* pSoldier); +INT8 DecideActionGreenSoldier(SOLDIERTYPE* pSoldier); +INT8 DecideActionYellowSoldier(SOLDIERTYPE* pSoldier); +INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier); +INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier); +INT8 DecideActionGreenRobot(SOLDIERTYPE* pSoldier); +INT8 DecideActionYellowRobot(SOLDIERTYPE* pSoldier); +INT8 DecideActionRedRobot(SOLDIERTYPE* pSoldier); +INT8 DecideActionBlackRobot(SOLDIERTYPE* pSoldier); + +INT8 DecideActionBlackSoldierUtilityAI(SOLDIERTYPE* pSoldier); INT16 DistanceToClosestFriend( SOLDIERTYPE * pSoldier ); @@ -240,7 +255,7 @@ void ManChecksOnFriends(SOLDIERTYPE *pSoldier); void NewDest(SOLDIERTYPE *pSoldier, INT32 sGridNo); INT32 NextPatrolPoint(SOLDIERTYPE *pSoldier); -INT8 PanicAI(SOLDIERTYPE *pSoldier, UINT8 ubCanMove); +ActionType PanicAI(SOLDIERTYPE *pSoldier, UINT8 ubCanMove); void HaltMoveForSoldierOutOfPoints(SOLDIERTYPE *pSoldier); INT32 RandDestWithinRange(SOLDIERTYPE *pSoldier); From e806fd37e7beda36bb7abf6b0425f8f5710abcd0 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:51:38 +0200 Subject: [PATCH 57/80] Add screen messages that display AI decisions --- TacticalAI/DecideAction.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 474ee9c1d..88b1b3908 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -7286,7 +7286,7 @@ INT8 DecideActionBlack(SOLDIERTYPE *pSoldier) pSoldier->name,sBestCover,iCoverPercentBetter); DebugAI( tempstr ) ; #endif - //ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_TESTVERSION, L"AI %d taking cover, morale %d, from %d to %d", pSoldier->ubID, pSoldier->aiData.bAIMorale, pSoldier->sGridNo, sBestCover ); + //ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d taking cover, morale %d, from %d to %d", pSoldier->ubID, pSoldier->aiData.bAIMorale, pSoldier->sGridNo, sBestCover ); pSoldier->aiData.usActionData = sBestCover; if(!TileIsOutOfBounds(sClosestOpponent))//dnl ch58 150913 After taking cover change facing toward recent target or closest enemy, currently such turn not charge APs and seems because AI is still in moving animation from take cover action { @@ -10207,7 +10207,7 @@ INT8 ArmedVehicleDecideActionBlack( SOLDIERTYPE *pSoldier ) if ( ((pSoldier->bTeam == MILITIA_TEAM) && ((INT16)(PreRandom( 20 )) > BestAttack.ubChanceToReallyHit)) || ((pSoldier->bTeam != MILITIA_TEAM) && ((INT16)(PreRandom( 40 )) > BestAttack.ubChanceToReallyHit)) ) { - //ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_TESTVERSION, L"AI %d allowing cover check, chance to hit is only %d, at range %d", BestAttack.ubChanceToReallyHit, PythSpacesAway( pSoldier->sGridNo, BestAttack.sTarget ) ); + //ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d allowing cover check, chance to hit is only %d, at range %d", BestAttack.ubChanceToReallyHit, PythSpacesAway( pSoldier->sGridNo, BestAttack.sTarget ) ); // maybe taking cover would be better! fAllowCoverCheck = TRUE; if ( (INT16)(PreRandom( 10 )) > BestAttack.ubChanceToReallyHit ) @@ -10747,6 +10747,7 @@ ActionType DecideActionStuckInWaterOrGas(SOLDIERTYPE *pSoldier, BOOLEAN ubCanMov if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) { DebugAI(AI_MSG_INFO, pSoldier, String("Move out of water towards closest opponent")); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d in deep water. Move towards closest opponent at grid %d", pSoldier->ubID.i, pSoldier->aiData.usActionData); return(AI_ACTION_LEAVE_WATER_GAS); } } @@ -10764,6 +10765,7 @@ ActionType DecideActionStuckInWaterOrGas(SOLDIERTYPE *pSoldier, BOOLEAN ubCanMov #endif DebugAI(AI_MSG_INFO, pSoldier, String("Leave for nearest (ungassed) land")); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d in water or gas. Move towards nearest safe grid %d", pSoldier->ubID.i, pSoldier->aiData.usActionData); return(AI_ACTION_LEAVE_WATER_GAS); } @@ -10780,6 +10782,7 @@ ActionType DecideActionStuckInWaterOrGas(SOLDIERTYPE *pSoldier, BOOLEAN ubCanMov #endif DebugAI(AI_MSG_INFO, pSoldier, String("NO LAND NEAR, RUNNING AWAY to grid %d", pSoldier->aiData.usActionData)); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d NO LAND NEAR, RUNNING AWAY to grid %d", pSoldier->ubID.i, pSoldier->aiData.usActionData); return(AI_ACTION_RUN_AWAY); } From 52a4959dfbf014e4e7c06b431cb0eb1268ec128a Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:56:18 +0200 Subject: [PATCH 58/80] Add optional InOut parameter to ClosestKnownOpponent distanceInCellCoords will return the distance between pSoldier and closest known opponent. We're often calculating same range after the call using PythSpacesAway and this way it's no longer necessary in most cases. --- TacticalAI/AIUtils.cpp | 42 +++++++++++++++++------------ TacticalAI/CreatureDecideAction.cpp | 12 +++++---- TacticalAI/DecideAction.cpp | 7 ++--- TacticalAI/ai.h | 3 ++- 4 files changed, 38 insertions(+), 26 deletions(-) diff --git a/TacticalAI/AIUtils.cpp b/TacticalAI/AIUtils.cpp index 76f4d017d..083ad3643 100644 --- a/TacticalAI/AIUtils.cpp +++ b/TacticalAI/AIUtils.cpp @@ -134,18 +134,17 @@ INT8 OKToAttack(SOLDIERTYPE * pSoldier, int target) BOOLEAN ConsiderProne( SOLDIERTYPE * pSoldier ) { - INT32 sOpponentGridNo; + INT32 sOpponentGridNo; INT8 bOpponentLevel; - INT32 iRange; + INT32 iRangeInCellCoords; if (pSoldier->aiData.bAIMorale >= MORALE_NORMAL) { return( FALSE ); } // We don't want to go prone if there is a nearby enemy - ClosestKnownOpponent( pSoldier, &sOpponentGridNo, &bOpponentLevel ); - iRange = PythSpacesAway( pSoldier->sGridNo, sOpponentGridNo ); - if (iRange > 10) + ClosestKnownOpponent( pSoldier, &sOpponentGridNo, &bOpponentLevel, nullptr, &iRangeInCellCoords); + if ( iRangeInCellCoords > 10*CELL_X_SIZE) { return( TRUE ); } @@ -397,7 +396,11 @@ UINT16 DetermineMovementMode( SOLDIERTYPE * pSoldier, INT8 bAction ) // sevenfm: movement mode tweaks if (gGameExternalOptions.fAIMovementMode) { - INT32 sClosestThreat = ClosestKnownOpponent(pSoldier, NULL, NULL); + INT32 distanceToThreat; + const INT32 sClosestThreat = ClosestKnownOpponent(pSoldier, NULL, NULL, NULL, &distanceToThreat); + const auto mediumRange = TACTICAL_RANGE_CELL_COORDS / 2; + const auto close = TACTICAL_RANGE_CELL_COORDS / 4; + const auto reallyClose = TACTICAL_RANGE_CELL_COORDS / 8; // use walking mode if no enemy known if (pSoldier->aiData.bAlertStatus < STATUS_RED && @@ -423,6 +426,7 @@ UINT16 DetermineMovementMode( SOLDIERTYPE * pSoldier, INT8 bAction ) (pSoldier->bTeam == ENEMY_TEAM || pSoldier->bTeam == MILITIA_TEAM)) { INT16 sDistanceVisible = VISION_RANGE; + const auto beyondVisionRange = (CELL_X_SIZE * 3 * sDistanceVisible) / 2; INT32 iRCD = RangeChangeDesire(pSoldier); // use running when in light at night @@ -445,7 +449,7 @@ UINT16 DetermineMovementMode( SOLDIERTYPE * pSoldier, INT8 bAction ) !GuySawEnemy(pSoldier) && (NightTime() || gAnimControl[pSoldier->usAnimState].ubEndHeight <= ANIM_CROUCH) && CountNearbyFriends(pSoldier, pSoldier->sGridNo, TACTICAL_RANGE / 4) < 3 && - PythSpacesAway(pSoldier->sGridNo, sClosestThreat) < 3 * sDistanceVisible / 2 && + distanceToThreat < beyondVisionRange && CountFriendsBlack(pSoldier) == 0 && bAction == AI_ACTION_SEEK_OPPONENT) { @@ -454,7 +458,7 @@ UINT16 DetermineMovementMode( SOLDIERTYPE * pSoldier, INT8 bAction ) // use swatting for taking cover if (pSoldier->aiData.bAlertStatus >= STATUS_RED && - PythSpacesAway(pSoldier->sGridNo, sClosestThreat) > (INT16)TACTICAL_RANGE / 8 && + distanceToThreat > reallyClose && (pSoldier->aiData.bUnderFire && iRCD < 4 || pSoldier->aiData.bShock > 2 * iRCD || pSoldier->aiData.bShock > 0 && gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_PRONE) && @@ -466,9 +470,9 @@ UINT16 DetermineMovementMode( SOLDIERTYPE * pSoldier, INT8 bAction ) // use SWATTING when under fire if (pSoldier->aiData.bAlertStatus >= STATUS_RED && - (pSoldier->aiData.bShock > iRCD && PythSpacesAway(pSoldier->sGridNo, sClosestThreat) > (INT16)TACTICAL_RANGE / 2 || - pSoldier->aiData.bShock > 0 && gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_PRONE && PythSpacesAway(pSoldier->sGridNo, sClosestThreat) > (INT16)TACTICAL_RANGE / 4) && - PythSpacesAway(pSoldier->sGridNo, sClosestThreat) < 3 * sDistanceVisible / 2 && + (pSoldier->aiData.bShock > iRCD && distanceToThreat > mediumRange || + pSoldier->aiData.bShock > 0 && gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_PRONE && distanceToThreat > close) && + distanceToThreat < beyondVisionRange && gAnimControl[pSoldier->usAnimState].ubEndHeight <= ANIM_CROUCH && !pSoldier->aiData.bLastAttackHit && (bAction == AI_ACTION_SEEK_OPPONENT || @@ -485,7 +489,7 @@ UINT16 DetermineMovementMode( SOLDIERTYPE * pSoldier, INT8 bAction ) (pSoldier->aiData.bOrders == SNIPER || pSoldier->aiData.bOrders == STATIONARY || (GuySawEnemy(pSoldier) || pSoldier->aiData.bShock > 0) && iRCD < 4) && - PythSpacesAway(pSoldier->sGridNo, sClosestThreat) > (INT16)TACTICAL_RANGE / 4 && + distanceToThreat > close && (bAction == AI_ACTION_SEEK_OPPONENT || bAction == AI_ACTION_GET_CLOSER || bAction == AI_ACTION_SEEK_FRIEND || @@ -501,7 +505,7 @@ UINT16 DetermineMovementMode( SOLDIERTYPE * pSoldier, INT8 bAction ) (pSoldier->aiData.bOrders == SNIPER || pSoldier->aiData.bOrders == STATIONARY || pSoldier->aiData.bShock > 0 && iRCD < 4) && - PythSpacesAway(pSoldier->sGridNo, sClosestThreat) > (INT16)TACTICAL_RANGE / 4 && + distanceToThreat > close && (bAction == AI_ACTION_SEEK_OPPONENT || bAction == AI_ACTION_GET_CLOSER || bAction == AI_ACTION_SEEK_FRIEND || @@ -515,7 +519,7 @@ UINT16 DetermineMovementMode( SOLDIERTYPE * pSoldier, INT8 bAction ) if (!pSoldier->aiData.bUnderFire && bAction == AI_ACTION_TAKE_COVER && pSoldier->bInitialActionPoints > APBPConstants[AP_MINIMUM] && - (!InARoom(pSoldier->sGridNo, NULL) || PythSpacesAway(pSoldier->sGridNo, sClosestThreat) > sDistanceVisible * 2) && + (!InARoom(pSoldier->sGridNo, NULL) || distanceToThreat > sDistanceVisible * 20) && pSoldier->aiData.bAIMorale >= MORALE_NORMAL && pSoldier->bBreath > 25 && pSoldier->pathing.bLevel == 0 && @@ -539,14 +543,14 @@ UINT16 DetermineMovementMode( SOLDIERTYPE * pSoldier, INT8 bAction ) else if (gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_CROUCH) { if (WeaponReady(pSoldier) && !pSoldier->aiData.bUnderFire && pSoldier->aiData.bAlertStatus == STATUS_BLACK || - pSoldier->aiData.bUnderFire && PythSpacesAway(pSoldier->sGridNo, sClosestThreat) > (INT16)TACTICAL_RANGE / 8) + pSoldier->aiData.bUnderFire && distanceToThreat > reallyClose ) return SWATTING; else return RUNNING; } else if (gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_PRONE) { - if (pSoldier->aiData.bUnderFire && !pSoldier->aiData.bLastAttackHit && PythSpacesAway(pSoldier->sGridNo, sClosestThreat) > (INT16)TACTICAL_RANGE / 8) + if (pSoldier->aiData.bUnderFire && !pSoldier->aiData.bLastAttackHit && distanceToThreat > reallyClose ) return SWATTING; else return RUNNING; @@ -1379,7 +1383,7 @@ INT32 ClosestReachableDisturbance(SOLDIERTYPE *pSoldier, BOOLEAN * pfChangeLevel } -INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel, SoldierID * pubOpponentID) +INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel, SoldierID * pubOpponentID, INT32 * distanceInCellCoords) { INT32 *psLastLoc,sGridNo, sClosestOpponent = NOWHERE; UINT32 uiLoop; @@ -1503,6 +1507,10 @@ INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLev { *pubOpponentID = pClosestOpponent->ubID; } + if ( distanceInCellCoords ) + { + *distanceInCellCoords = iClosestRange; + } return( sClosestOpponent ); } diff --git a/TacticalAI/CreatureDecideAction.cpp b/TacticalAI/CreatureDecideAction.cpp index 0d655ffed..cd95d4059 100644 --- a/TacticalAI/CreatureDecideAction.cpp +++ b/TacticalAI/CreatureDecideAction.cpp @@ -844,7 +844,8 @@ INT8 CreatureDecideActionRed(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK) { // determine the location of the known closest opponent // (don't care if he's conscious, don't care if he's reachable at all) - sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); + INT32 distanceToThreat; + sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL, NULL, &distanceToThreat); if (!TileIsOutOfBounds(sClosestOpponent)) { @@ -854,9 +855,9 @@ INT8 CreatureDecideActionRed(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK) // if soldier is not already facing in that direction, // and the opponent is close enough that he could possibly be seen // note, have to change this to use the level returned from ClosestKnownOpponent - sDistVisible = pSoldier->GetMaxDistanceVisible(sClosestOpponent, 0 ); + sDistVisible = pSoldier->GetMaxDistanceVisible(sClosestOpponent, 0 ) * CELL_X_SIZE; - if ((pSoldier->ubDirection != ubOpponentDir) && (PythSpacesAway(pSoldier->sGridNo,sClosestOpponent) <= sDistVisible)) + if ((pSoldier->ubDirection != ubOpponentDir) && (distanceToThreat <= sDistVisible)) { // set base chance according to orders if ((pSoldier->aiData.bOrders == STATIONARY) || (pSoldier->aiData.bOrders == ONGUARD)) @@ -1380,11 +1381,12 @@ INT8 CreatureDecideActionBlack( SOLDIERTYPE * pSoldier ) { // determine the location of the known closest opponent // (don't care if he's conscious, don't care if he's reachable at all) - sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); + INT32 distanceToThreat; + sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL, NULL, &distanceToThreat); // if we have a closest reachable opponent if (!TileIsOutOfBounds(sClosestOpponent)) { - if ( ubCanMove && PythSpacesAway( pSoldier->sGridNo, sClosestOpponent ) > 2 ) + if ( ubCanMove && distanceToThreat > 20 ) // 2 tiles -> 20 in fractional Cell Coordinates { if ( bSpitIn != NO_SLOT ) { diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 88b1b3908..5a0150c6d 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -9637,7 +9637,8 @@ INT8 ArmedVehicleDecideActionRed( SOLDIERTYPE *pSoldier) { // determine the location of the known closest opponent // (don't care if he's conscious, don't care if he's reachable at all) - sClosestOpponent = ClosestKnownOpponent( pSoldier, NULL, NULL ); + INT32 distanceToOpponent; + sClosestOpponent = ClosestKnownOpponent( pSoldier, NULL, NULL, NULL, &distanceToOpponent ); if ( !TileIsOutOfBounds( sClosestOpponent ) ) { @@ -9647,9 +9648,9 @@ INT8 ArmedVehicleDecideActionRed( SOLDIERTYPE *pSoldier) // if soldier is not already facing in that direction, // and the opponent is close enough that he could possibly be seen // note, have to change this to use the level returned from ClosestKnownOpponent - sDistVisible = pSoldier->GetMaxDistanceVisible( sClosestOpponent, 0, CALC_FROM_ALL_DIRS ); + sDistVisible = pSoldier->GetMaxDistanceVisible( sClosestOpponent, 0, CALC_FROM_ALL_DIRS ) * CELL_X_SIZE; - if ( (pSoldier->ubDirection != ubOpponentDir) && (PythSpacesAway( pSoldier->sGridNo, sClosestOpponent ) <= sDistVisible) ) + if ( (pSoldier->ubDirection != ubOpponentDir) && (distanceToOpponent <= sDistVisible) ) { // set base chance according to orders if ( (pSoldier->aiData.bOrders == STATIONARY) || (pSoldier->aiData.bOrders == ONGUARD) ) diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index 3864cde7b..a450cd240 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -171,7 +171,7 @@ void CheckForChangingOrders(SOLDIERTYPE *pSoldier ); INT8 ClosestPanicTrigger( SOLDIERTYPE * pSoldier ); -INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel, SoldierID *pubOpponentID = NULL); +INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel, SoldierID *pubOpponentID = NULL, INT32 * distanceInCellCoords = NULL); INT32 ClosestPC( SOLDIERTYPE *pSoldier, INT32 * psDistance ); INT32 ClosestUnDisguisedPC( SOLDIERTYPE *pSoldier, INT32 * psDistance ); // Flugente: like ClosestPC(...), but does not account for covert or not visible mercs BOOLEAN CanAutoBandage( BOOLEAN fDoFullCheck ); @@ -431,6 +431,7 @@ INT8 KnownPublicLevel(UINT8 bTeam, SoldierID ubOpponentID); // sevenfm: distance for tactical AI checks, roughly equal to normal day vision range #define TACTICAL_RANGE (gGameExternalOptions.ubStraightSightRange * STRAIGHT_RATIO * 2) +#define TACTICAL_RANGE_CELL_COORDS TACTICAL_RANGE*CELL_X_SIZE #define BOMB_DETECTION_RANGE (TACTICAL_RANGE / 4) #endif From df8d4bd8a25bf4fba870725e45b20043f61078ed Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:57:34 +0200 Subject: [PATCH 59/80] Only show message if temperature has changed This was absolutely clogging Debug build when compressing time --- Strategic/Map Screen Interface Map Inventory.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Strategic/Map Screen Interface Map Inventory.cpp b/Strategic/Map Screen Interface Map Inventory.cpp index 41c7a82e1..d6ef82395 100644 --- a/Strategic/Map Screen Interface Map Inventory.cpp +++ b/Strategic/Map Screen Interface Map Inventory.cpp @@ -6024,7 +6024,10 @@ void HandleItemCooldownFunctions( OBJECTTYPE* itemStack, INT32 deltaSeconds, BOO FLOAT newguntemperature = max(0.0f, guntemperature - tickspassed * cooldownfactor ); // ... calculate new temperature ... #if JA2TESTVERSION - ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, L"World: Item temperature lowered from %4.2f to %4.2f", guntemperature, newguntemperature); + if (guntemperature != newguntemperature) + { + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, L"World: Item temperature lowered from %4.2f to %4.2f", guntemperature, newguntemperature); + } #endif (*itemStack)[i]->data.bTemperature = newguntemperature; // ... set new temperature @@ -6044,7 +6047,10 @@ void HandleItemCooldownFunctions( OBJECTTYPE* itemStack, INT32 deltaSeconds, BOO (*iter)[i]->data.bTemperature = newtemperature; // ... set new temperature #if JA2TESTVERSION - ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, L"World: Item temperature lowered from %4.2f to %4.2f", temperature, newtemperature); + if (temperature != newtemperature) + { + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, L"World: Item temperature lowered from %4.2f to %4.2f", temperature, newtemperature); + } #endif // we assume that there can exist only 1 underbarrel weapon per gun From 1b9cb1f75e8b22b3dc53612c1aff3b85576636da Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:58:17 +0200 Subject: [PATCH 60/80] Add functions to check if soldier is in specific gas --- TacticalAI/AIInternals.h | 3 +++ TacticalAI/AIUtils.cpp | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/TacticalAI/AIInternals.h b/TacticalAI/AIInternals.h index 2e6d35ead..b0edaabbd 100644 --- a/TacticalAI/AIInternals.h +++ b/TacticalAI/AIInternals.h @@ -242,6 +242,9 @@ INT32 GoAsFarAsPossibleTowards(SOLDIERTYPE *pSoldier, INT32 sDesGrid, INT8 bActi ActionType HeadForTheStairCase( SOLDIERTYPE * pSoldier ); BOOLEAN InSmoke(INT32 sGridNo, INT8 bLevel); +BOOLEAN InSmoke(SOLDIERTYPE* pSoldier, INT32 sGridNo); +BOOLEAN InTearGas(SOLDIERTYPE* pSoldier, INT32 sGridNo); +BOOLEAN InMustardGas(SOLDIERTYPE* pSoldier, INT32 sGridNo); BOOLEAN InGas( SOLDIERTYPE *pSoldier, INT32 sGridNo ); BOOLEAN InGasOrSmoke( SOLDIERTYPE *pSoldier, INT32 sGridNo ); BOOLEAN InWaterGasOrSmoke( SOLDIERTYPE *pSoldier, INT32 sGridNo ); diff --git a/TacticalAI/AIUtils.cpp b/TacticalAI/AIUtils.cpp index 083ad3643..5ddef11b3 100644 --- a/TacticalAI/AIUtils.cpp +++ b/TacticalAI/AIUtils.cpp @@ -2240,6 +2240,31 @@ INT16 DistanceToClosestFriend( SOLDIERTYPE * pSoldier ) return( sMinDist ); } +BOOLEAN InSmoke(SOLDIERTYPE* pSoldier, INT32 sGridNo) +{ + if ( gpWorldLevelData[sGridNo].ubExtFlags[pSoldier->pathing.bLevel] & (MAPELEMENT_EXT_SMOKE | MAPELEMENT_EXT_SIGNAL_SMOKE | MAPELEMENT_EXT_DEBRIS_SMOKE | MAPELEMENT_EXT_FIRERETARDANT_SMOKE) ) + return TRUE; + + return FALSE; +} + +BOOLEAN InTearGas(SOLDIERTYPE* pSoldier, INT32 sGridNo) +{ + if ( (gpWorldLevelData[sGridNo].ubExtFlags[pSoldier->pathing.bLevel] & MAPELEMENT_EXT_TEARGAS)) + return TRUE; + + return FALSE; +} + +BOOLEAN InMustardGas(SOLDIERTYPE* pSoldier, INT32 sGridNo) +{ + if ( gpWorldLevelData[sGridNo].ubExtFlags[pSoldier->pathing.bLevel] & (MAPELEMENT_EXT_BURNABLEGAS | MAPELEMENT_EXT_CREATUREGAS | MAPELEMENT_EXT_MUSTARDGAS) ) + return TRUE; + + return FALSE; +} + + BOOLEAN InWaterGasOrSmoke( SOLDIERTYPE *pSoldier, INT32 sGridNo ) { if (WaterTooDeepForAttacks( sGridNo, pSoldier->pathing.bLevel )) From 4678cab16fb95f653f957b6ebf26a42e84688585 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:58:33 +0200 Subject: [PATCH 61/80] Use correct type --- TacticalAI/DecideAction.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 5a0150c6d..8f86d2bf8 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -815,7 +815,7 @@ INT8 DecideActionNamedNPC( SOLDIERTYPE * pSoldier ) INT8 DecideActionGreen(SOLDIERTYPE *pSoldier) { - DOUBLE iChance, iSneaky = 10; + INT32 iChance, iSneaky = 10; INT8 bInWater, bInDeepWater, bInGas; #ifdef DEBUGDECISIONS STR16 tempstr; @@ -7774,7 +7774,7 @@ INT8 ArmedVehicleDecideAction( SOLDIERTYPE *pSoldier ) INT8 ArmedVehicleDecideActionGreen( SOLDIERTYPE *pSoldier ) { - DOUBLE iChance, iSneaky = 10; + INT32 iChance, iSneaky = 10; INT8 bInWater; #ifdef DEBUGDECISIONS STR16 tempstr; From 58ea3be2fd9b6b9c21d9eb4568d120684aeba3e8 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:58:58 +0200 Subject: [PATCH 62/80] Remove MercPtrs[] SoldierID works without it thanks to operator overload --- TacticalAI/DecideAction.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 8f86d2bf8..656994139 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -3004,7 +3004,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) if (BestThrow.ubOpponent != NOBODY && !BestThrow.ubOpponent->IsFlanking()) { DebugAI(AI_MSG_INFO, pSoldier, String("start retreat counter for %d", BestThrow.ubOpponent), gLogDecideActionRed); - MercPtrs[BestThrow.ubOpponent]->RetreatCounterStart(2); + BestThrow.ubOpponent->RetreatCounterStart(2); } // if necessary, swap the usItem from holster into the hand position From ff96d653e2ed5258ff02c124c473441248cf2c3a Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:59:30 +0200 Subject: [PATCH 63/80] Add skeleton for utilityAI framework --- TacticalAI/CMakeLists.txt | 2 + TacticalAI/UtilityAI.cpp | 372 +++++++++++++++++++++++++ TacticalAI/UtilityAI.h | 5 + TacticalAI/UtilityAI_ResponseCurve.cpp | 57 ++++ TacticalAI/UtilityAI_ResponseCurve.h | 24 ++ 5 files changed, 460 insertions(+) create mode 100644 TacticalAI/UtilityAI.cpp create mode 100644 TacticalAI/UtilityAI.h create mode 100644 TacticalAI/UtilityAI_ResponseCurve.cpp create mode 100644 TacticalAI/UtilityAI_ResponseCurve.h diff --git a/TacticalAI/CMakeLists.txt b/TacticalAI/CMakeLists.txt index 53ef7ec00..106cf5474 100644 --- a/TacticalAI/CMakeLists.txt +++ b/TacticalAI/CMakeLists.txt @@ -14,4 +14,6 @@ set(TacticalAISrc "${CMAKE_CURRENT_SOURCE_DIR}/QuestDebug.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Realtime.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/ZombieDecideAction.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/UtilityAI.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/UtilityAI_ResponseCurve.cpp" PARENT_SCOPE) diff --git a/TacticalAI/UtilityAI.cpp b/TacticalAI/UtilityAI.cpp new file mode 100644 index 000000000..c6b707969 --- /dev/null +++ b/TacticalAI/UtilityAI.cpp @@ -0,0 +1,372 @@ +#include "UtilityAI.h" +#include "UtilityAI_ResponseCurve.h" +#include "Overhead Types.h" +#include "Soldier Control.h" +#include "Isometric Utils.h" +#include "soldier profile type.h" +#include "AIInternals.h" +#include "Render Fun.h" + +namespace UtilityAI +{ + + typedef enum DecisionInput + { + MyHealth, + MyStamina, + MyStance, + MyMorale, + MyCurrentWeaponRange, + DistanceFromMe, + HealthOfTarget, + AllyCount, + EnemyCount, + AllyEnemyRatio, + EnemiesNearTarget, + AlliesNearTarget, + HaveGasmaskInInventory, + HaveNVGInInventory, + HasKnife, + HasThrowingKnife, + HasSmokeGrenade, + IamInSmoke, + IamInTearGas, + IamInMustardGas, // Also fire & creature gas + NearSmoke, + NearTearGas, + NearMustardGas, + InShock, + InsideRoom, + NightTime, + InLightAtNight, + TakenLargeHit, + HasTrait_AutoWeapons, + HasTrait_HeavyWeapons, + HasTrait_Sniper, + HasTrait_Ranger, + HasTrait_Gunslinger, + HasTrait_MartialArts, + HasTrait_SquadLeader, + HasTrait_Technician, + HasTrait_Doctor, + HasTrait_Ambidextrous, + HasTrait_Melee, + HasTrait_Throwing, + HasTrait_NightOps, + HasTrait_Stealthy, + HasTrait_Athletics, + HasTrait_Bodybuilding, + HasTrait_Demolitions, + HasTrait_Teaching, + HasTrait_Scouting, + HasTrait_Covert, + HasTrait_RadioOperator, + HasTrait_Snitch, + HasTrait_Survival, + HasTraitOld_Lockpicking, + HasTraitOld_HandToHand, + HasTraitOld_Electronics, + HasTraitOld_NightOps, + HasTraitOld_Throwing, + HasTraitOld_Teaching, + HasTraitOld_HeavyWeapons, + HasTraitOld_AutoWeapons, + HasTraitOld_Stealthy, + HasTraitOld_Ambidextrous, + HasTraitOld_Thief, + HasTraitOld_MartialArts, + HasTraitOld_Knifing, + HasTraitOld_Sniper, + HasTraitOld_Camouflaged, + + }; + + struct Consideration + { + DecisionInput input; + ResponseCurve curve; + SoldierID target; + INT32 targetLocation; + }; + + struct DecisionScoreEvaluator + { + ActionType Action; + float priorityWeight; + std::string description; + std::vector considerations; + }; + + struct DecisionScore + { + float score; + size_t idx; // Index into DM::decisions + + bool operator<(const DecisionScore& a) const + { + return score < a.score; + } + + bool operator>(const DecisionScore& a) const + { + return score > a.score; + } + + bool operator==(const DecisionScore& a) const + { + return score == a.score; + } + + }; + + struct DecisionMaker + { + std::vector decisions; + std::vector scores; + }; + + static float GetTrait(DecisionInput input, SOLDIERTYPE* soldier) + { + switch ( input ) + { + case HasTrait_AutoWeapons: + return static_cast(HAS_SKILL_TRAIT(soldier, AUTO_WEAPONS_NT)); + case HasTrait_HeavyWeapons: + return static_cast(HAS_SKILL_TRAIT(soldier, HEAVY_WEAPONS_NT)); + case HasTrait_Sniper: + return static_cast(HAS_SKILL_TRAIT(soldier, SNIPER_NT)); + case HasTrait_Ranger: + return static_cast(HAS_SKILL_TRAIT(soldier, RANGER_NT)); + case HasTrait_Gunslinger: + return static_cast(HAS_SKILL_TRAIT(soldier, GUNSLINGER_NT)); + case HasTrait_MartialArts: + return static_cast(HAS_SKILL_TRAIT(soldier, MARTIAL_ARTS_NT)); + case HasTrait_SquadLeader: + return static_cast(HAS_SKILL_TRAIT(soldier, SQUADLEADER_NT)); + case HasTrait_Technician: + return static_cast(HAS_SKILL_TRAIT(soldier, TECHNICIAN_NT)); + case HasTrait_Doctor: + return static_cast(HAS_SKILL_TRAIT(soldier, DOCTOR_NT)); + case HasTrait_Ambidextrous: + return static_cast(HAS_SKILL_TRAIT(soldier, AMBIDEXTROUS_NT)); + case HasTrait_Melee: + return static_cast(HAS_SKILL_TRAIT(soldier, MELEE_NT)); + case HasTrait_Throwing: + return static_cast(HAS_SKILL_TRAIT(soldier, THROWING_NT)); + case HasTrait_NightOps: + return static_cast(HAS_SKILL_TRAIT(soldier, NIGHT_OPS_NT)); + case HasTrait_Stealthy: + return static_cast(HAS_SKILL_TRAIT(soldier, STEALTHY_NT)); + case HasTrait_Athletics: + return static_cast(HAS_SKILL_TRAIT(soldier, ATHLETICS_NT)); + case HasTrait_Bodybuilding: + return static_cast(HAS_SKILL_TRAIT(soldier, BODYBUILDING_NT)); + case HasTrait_Demolitions: + return static_cast(HAS_SKILL_TRAIT(soldier, DEMOLITIONS_NT)); + case HasTrait_Teaching: + return static_cast(HAS_SKILL_TRAIT(soldier, TEACHING_NT)); + case HasTrait_Scouting: + return static_cast(HAS_SKILL_TRAIT(soldier, SCOUTING_NT)); + case HasTrait_Covert: + return static_cast(HAS_SKILL_TRAIT(soldier, COVERT_NT)); + case HasTrait_RadioOperator: + return static_cast(HAS_SKILL_TRAIT(soldier, RADIO_OPERATOR_NT)); + case HasTrait_Snitch: + return static_cast(HAS_SKILL_TRAIT(soldier, SNITCH_NT)); + case HasTrait_Survival: + return static_cast(HAS_SKILL_TRAIT(soldier, SURVIVAL_NT)); + case HasTraitOld_Lockpicking: + return static_cast(HAS_SKILL_TRAIT(soldier, LOCKPICKING_OT)); + case HasTraitOld_HandToHand: + return static_cast(HAS_SKILL_TRAIT(soldier, HANDTOHAND_OT)); + case HasTraitOld_Electronics: + return static_cast(HAS_SKILL_TRAIT(soldier, ELECTRONICS_OT)); + case HasTraitOld_NightOps: + return static_cast(HAS_SKILL_TRAIT(soldier, NIGHTOPS_OT)); + case HasTraitOld_Throwing: + return static_cast(HAS_SKILL_TRAIT(soldier, THROWING_OT)); + case HasTraitOld_Teaching: + return static_cast(HAS_SKILL_TRAIT(soldier, TEACHING_OT)); + case HasTraitOld_HeavyWeapons: + return static_cast(HAS_SKILL_TRAIT(soldier, HEAVY_WEAPS_OT)); + case HasTraitOld_AutoWeapons: + return static_cast(HAS_SKILL_TRAIT(soldier, AUTO_WEAPS_OT)); + case HasTraitOld_Stealthy: + return static_cast(HAS_SKILL_TRAIT(soldier, STEALTHY_OT)); + case HasTraitOld_Ambidextrous: + return static_cast(HAS_SKILL_TRAIT(soldier, AMBIDEXT_OT)); + case HasTraitOld_Thief: + return static_cast(HAS_SKILL_TRAIT(soldier, THIEF_OT)); + case HasTraitOld_MartialArts: + return static_cast(HAS_SKILL_TRAIT(soldier, MARTIALARTS_OT)); + case HasTraitOld_Knifing: + return static_cast(HAS_SKILL_TRAIT(soldier, KNIFING_OT)); + case HasTraitOld_Sniper: + return static_cast(HAS_SKILL_TRAIT(soldier, PROF_SNIPER_OT)); + case HasTraitOld_Camouflaged: + return static_cast(HAS_SKILL_TRAIT(soldier, CAMOUFLAGED_OT)); + } + + return 0.0f; + } + + float GetInputValue(DecisionInput input, SOLDIERTYPE* me, SoldierID target, INT32 targetLocation) + { + switch ( input ) + { + case MyHealth: + { + const auto health = static_cast(me->stats.bLife); + const auto min = 0.0f; + const auto max = static_cast(me->stats.bLifeMax); + return NormalizeInput(health, min, max); + } + case MyMorale: + { + const auto morale = static_cast(me->aiData.bAIMorale); + const auto min = 0.0f; + const auto max = static_cast(MORALE_FEARLESS); + return NormalizeInput(morale, min, max); + } + case HaveGasmaskInInventory: + { + const auto gasmaskInvSlot = FindGasMask(me); + if ( gasmaskInvSlot != NO_SLOT ) { return 1.0f; } + else { return 0.0f; } + } + case HasKnife: + { + const auto bWeaponIn = FindAIUsableObjClass(me, (IC_BLADE)); + if ( bWeaponIn != NO_SLOT ) { return 1.0f; } + else { return 0.0f; } + } + case HasThrowingKnife: + { + const auto bWeaponIn = FindAIUsableObjClass(me, (IC_THROWING_KNIFE)); + if ( bWeaponIn != NO_SLOT ) { return 1.0f; } + else { return 0.0f; } + } + case HasSmokeGrenade: + { + const auto bGrenadeIn = FindThrowableGrenade(me, EXPLOSV_SMOKE); + if ( bGrenadeIn != NO_SLOT ) { return 1.0f; } + else { return 0.0f; } + } + case DistanceFromMe: + { + const auto distance = PythSpacesAway(me->sGridNo, targetLocation); + } + case IamInSmoke: + return static_cast(InSmoke(me, me->sGridNo)); + case IamInTearGas: + return static_cast(InTearGas(me, me->sGridNo)); + case IamInMustardGas: + return static_cast(InMustardGas(me, me->sGridNo)); + case NearSmoke: + + case NearTearGas: + + case NearMustardGas: + + case InShock: + + case InsideRoom: + return static_cast(InARoom(me->sGridNo, NULL)); + case NightTime: + + case InLightAtNight: + + case TakenLargeHit: + return static_cast(me->TakenLargeHit()); + + case HasTrait_AutoWeapons: + case HasTrait_HeavyWeapons: + case HasTrait_Sniper: + case HasTrait_Ranger: + case HasTrait_Gunslinger: + case HasTrait_MartialArts: + case HasTrait_SquadLeader: + case HasTrait_Technician: + case HasTrait_Doctor: + case HasTrait_Ambidextrous: + case HasTrait_Melee: + case HasTrait_Throwing: + case HasTrait_NightOps: + case HasTrait_Stealthy: + case HasTrait_Athletics: + case HasTrait_Bodybuilding: + case HasTrait_Demolitions: + case HasTrait_Teaching: + case HasTrait_Scouting: + case HasTrait_Covert: + case HasTrait_RadioOperator: + case HasTrait_Snitch: + case HasTrait_Survival: + case HasTraitOld_Lockpicking: + case HasTraitOld_HandToHand: + case HasTraitOld_Electronics: + case HasTraitOld_NightOps: + case HasTraitOld_Throwing: + case HasTraitOld_Teaching: + case HasTraitOld_HeavyWeapons: + case HasTraitOld_AutoWeapons: + case HasTraitOld_Stealthy: + case HasTraitOld_Ambidextrous: + case HasTraitOld_Thief: + case HasTraitOld_MartialArts: + case HasTraitOld_Knifing: + case HasTraitOld_Sniper: + case HasTraitOld_Camouflaged: + return GetTrait(input, me); + } + } + + float ScoreDSE(DecisionScoreEvaluator DSE, SOLDIERTYPE* me) + { + float finalScore = 1.0f; + + for ( const auto& consideration : DSE.considerations ) + { + if ( finalScore <= 0.0f ) { break; } + + float curveInput = GetInputValue(consideration.input, me, consideration.target, consideration.targetLocation); + float considerationScore = CalculateResponse(curveInput, consideration.curve); + + finalScore *= considerationScore; + } + + // Add consideration compensation + const auto modificationFactor = 1 - 1 / (DSE.considerations.size()); + const auto makeUpValue = (1 - finalScore) * modificationFactor; + finalScore += finalScore * makeUpValue; + + // Add DSE priority weight + finalScore *= DSE.priorityWeight; + + return finalScore; + } + + void ScoreAllDecisions(DecisionMaker DM, SOLDIERTYPE* me) + { + size_t idx = 0; + for ( const auto& decision : DM.decisions ) + { + DM.scores[idx].score = ScoreDSE(decision, me); + DM.scores[idx].idx = idx; + ++idx; + } + + // Sort all scores from best to last + std::sort(DM.scores.begin(), DM.scores.end(), std::greater()); + } + + ActionType SelectScoredAction(DecisionMaker DM) + { + // Select highest for now. TODO weighted random choice + const auto idx = DM.scores[0].idx; + const auto action = DM.decisions[idx].Action; + + return action; + } + +} diff --git a/TacticalAI/UtilityAI.h b/TacticalAI/UtilityAI.h new file mode 100644 index 000000000..56ac7cca2 --- /dev/null +++ b/TacticalAI/UtilityAI.h @@ -0,0 +1,5 @@ +#pragma once + +//void ScoreAllDecisions(DecisionMaker DM, SOLDIERTYPE* me); +//ActionType SelectScoredAction(DecisionMaker DM); + diff --git a/TacticalAI/UtilityAI_ResponseCurve.cpp b/TacticalAI/UtilityAI_ResponseCurve.cpp new file mode 100644 index 000000000..d0f5aa84b --- /dev/null +++ b/TacticalAI/UtilityAI_ResponseCurve.cpp @@ -0,0 +1,57 @@ +#include "UtilityAI_ResponseCurve.h" + +#include +#include + +namespace UtilityAI +{ + + float NormalizeInput(float value, float min, float max) + { + const float normalizedValue = (value - min) / (max - min); + return normalizedValue; + } + + float CalculateResponse(float x, ResponseCurve curve) + { + const auto min = curve.min; + const auto max = curve.max; + const auto type = curve.type; + const auto m = curve.m; + const auto k = curve.k; + const auto b = curve.b; + const auto c = curve.c; + + float input = std::clamp(x, min, max); + input = NormalizeInput(input, min, max); + + float y = 0.0f; + // Polynomial + float n; + // Logistic + const float e = 2.71828f; // Euler's number. I think? Lecture slides did not explain if this is what it's supposed to be. + const float exponent = -input + c; + const float denominator = 1.0f + 1000.0f * e * pow(m, exponent); + + switch ( type ) + { + case Step: + if ( input != 0.0f ) { return 1.0f; } + else { return 0.0f; } + case Linear: + case Polynomial: + n = input - c; + y = m * pow(n, k) + b; + break; + case Logistic: + y = (k / denominator) + b; + break; + //case Logit: // Not implemented yet + //break; + } + + y = std::clamp(y, 0.0f, 1.0f); + return y; + } + +} diff --git a/TacticalAI/UtilityAI_ResponseCurve.h b/TacticalAI/UtilityAI_ResponseCurve.h new file mode 100644 index 000000000..e50028429 --- /dev/null +++ b/TacticalAI/UtilityAI_ResponseCurve.h @@ -0,0 +1,24 @@ +#pragma once + +typedef enum +{ + Step, + Linear, + Polynomial, + Logistic, + Logit +} ResponseCurveType; + +struct ResponseCurve +{ + ResponseCurveType type; + float m; + float k; + float b; + float c; + float min; + float max; +}; + +float NormalizeInput(float value, float min, float max); +float CalculateResponse(float x, ResponseCurve curve); From 4ad34f80a92c95ef8ae13090d8fd3130469afdb8 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:59:55 +0200 Subject: [PATCH 64/80] Fix includes --- TacticalAI/DecideAction.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 656994139..a3a268c00 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -16,7 +16,6 @@ #include "PATHAI.H" #include "Render Fun.h" #include "Boxing.h" -// #include "Air Raid.h" #include "Soldier Profile.h" #include "soldier profile type.h" #include "Soldier macros.h" @@ -28,12 +27,12 @@ #include "Soldier Ani.h" #include "Rotting Corpses.h" #include "GameSettings.h" -#include "Dialogue Control.h" #include "connect.h" #include "Text.h" #include "Exit Grids.h" // added by Flugente #include "Game Clock.h" // sevenfm #include "SkillCheck.h" // sevenfm +#include "UtilityAI.h" ////////////////////////////////////////////////////////////////////////////// // SANDRO - In this file, all APBPConstants[AP_CROUCH] and APBPConstants[AP_PRONE] were changed to GetAPsCrouch() and GetAPsProne() From 2b1838dbb0a888baa84568ac0fd5e7e1164b71ef Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Mon, 19 Jan 2026 23:34:19 +0200 Subject: [PATCH 65/80] Remove unused local variable --- TacticalAI/AIUtils.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/TacticalAI/AIUtils.cpp b/TacticalAI/AIUtils.cpp index 5ddef11b3..2ea42f281 100644 --- a/TacticalAI/AIUtils.cpp +++ b/TacticalAI/AIUtils.cpp @@ -3941,8 +3941,6 @@ INT8 CalcMoraleNew(SOLDIERTYPE *pSoldier) bMoraleCategory ++; } - INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); - // if last attack of this soldier hit enemy - increase morale if( pSoldier->aiData.bLastAttackHit ) { From 76af6b764127c86902a253c92ffbe2eea15c5cbc Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:23:52 +0200 Subject: [PATCH 66/80] Remove duplicate include --- TacticalAI/FindLocations.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/TacticalAI/FindLocations.cpp b/TacticalAI/FindLocations.cpp index 967b6efa2..d8ffde6de 100644 --- a/TacticalAI/FindLocations.cpp +++ b/TacticalAI/FindLocations.cpp @@ -15,7 +15,6 @@ #include "Render Fun.h" #include "Boxing.h" #include "Text.h" - #include "renderworld.h" #include "worldman.h" #include "strategicmap.h" #include "environment.h" From 1419cc6925812ef98310bee4f3c7663e31b989dd Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:24:53 +0200 Subject: [PATCH 67/80] Avoid overcrowding when looking for spot to move to --- TacticalAI/FindLocations.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/TacticalAI/FindLocations.cpp b/TacticalAI/FindLocations.cpp index d8ffde6de..895db4d41 100644 --- a/TacticalAI/FindLocations.cpp +++ b/TacticalAI/FindLocations.cpp @@ -655,7 +655,7 @@ static void CalculateCoverValue(SOLDIERTYPE* pSoldier, const INT32 sGridNo, cons } // sevenfm: check for nearby friends, add bonus/penalty 10% - UINT8 ubNearbyFriends = __max(0, CountNearbyFriends(pSoldier, sGridNo, 7)); + UINT8 ubNearbyFriends = __max(0, CountNearbyFriends(pSoldier, sGridNo, TACTICAL_RANGE_CLOSE)); iCoverValue -= ubNearbyFriends * abs(iCoverValue) / (6 - ubDiff); // sevenfm: penalize locations with fresh corpses @@ -963,6 +963,13 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB continue; } + // Avoid overcrowding + const auto nearbyFriends = CountNearbyFriends(pSoldier, sGridNo, TACTICAL_RANGE_VERYCLOSE); + if ( nearbyFriends > 1 && !pSoldier->IsZombie() ) + { + continue; + } + // zombies only want to hide if (pSoldier->IsZombie() && !SightCoverAtSpot(pSoldier, sGridNo, TRUE)) { @@ -3122,7 +3129,8 @@ INT32 FindAdvanceSpot(SOLDIERTYPE *pSoldier, INT32 sTargetSpot, INT8 bAction, UI } // avoid overcrowding - if (!pSoldier->IsZombie() && NumberOfTeamMatesAdjacent(pSoldier, sGridNo) > 1) + const auto nearbyFriends = CountNearbyFriends(pSoldier, sGridNo, TACTICAL_RANGE_VERYCLOSE); + if ( nearbyFriends > 1 && !pSoldier->IsZombie() ) { continue; } From 42215e7a51b96f1cc6ed2138ee5775d6e1bb888e Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:25:33 +0200 Subject: [PATCH 68/80] Remove magic numbers --- TacticalAI/DecideAction.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index a3a268c00..b2b94a4e3 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -21391,11 +21391,11 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) (ubBestAttackAction == AI_ACTION_NONE || ubBestAttackAction == AI_ACTION_FIRE_GUN && BestAttack.ubChanceToReallyHit < 5 * RangeChangeDesire(pSoldier)) && AIGunRange(pSoldier) < DAY_VISION_RANGE && pSoldier->aiData.bAIMorale >= MORALE_NORMAL && - (!fAnyCover || !fProneSightCover || AIGunRange(pSoldier)*10 < distanceToOpponent) && + (!fAnyCover || !fProneSightCover || AIGunRange(pSoldier)*CELL_X_SIZE < distanceToOpponent) && distanceToOpponent < 10*MAX_VISION_RANGE && //DetermineMovementMode(pSoldier, AI_ACTION_GET_CLOSER) != CRAWLING && pSoldier->aiData.bShock < RangeChangeDesire(pSoldier) * 2 && - (AIGunRange(pSoldier)*10 < distanceToOpponent || + (AIGunRange(pSoldier)*CELL_X_SIZE < distanceToOpponent || pSoldier->aiData.bLastAttackHit && pSoldier->sLastTarget != NOWHERE || pSoldier->aiData.bAIMorale == MORALE_FEARLESS || ubBestAttackAction == AI_ACTION_NONE || From 2448ccb1919201ab5b666cdd2b8ba280d8c1afe5 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:26:18 +0200 Subject: [PATCH 69/80] Tweak AI screen messages --- TacticalAI/DecideAction.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index b2b94a4e3..06fa7fc16 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -21366,7 +21366,7 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) if (!TileIsOutOfBounds(sRetreatSpot)) { DebugAI(AI_MSG_TOPIC, pSoldier, String("found retreat spot %d", sRetreatSpot)); - ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d status black retreat, target grid %d", pSoldier->ubID.i, sRetreatSpot); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d BLACK retreat, target grid %d", pSoldier->ubID.i, sRetreatSpot); //BeginMultiPurposeLocator(sRetreatSpot, pSoldier->pathing.bLevel, FALSE); @@ -21426,11 +21426,10 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) if (pSoldier->aiData.usActionData != NOWHERE) { DebugAI(AI_MSG_INFO, pSoldier, String("cover advance spot ok")); - ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d status black advance to cover, target grid %d", pSoldier->ubID.i, sAdvanceSpot); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d BLACK advance to cover, target grid %d", pSoldier->ubID.i, sAdvanceSpot); pSoldier->aiData.usActionData = sAdvanceSpot; - //ScreenMsg(FONT_MCOLOR_LTGREEN, MSG_INTERFACE, L"[%d] found cover advance spot %d", pSoldier->ubID, sAdvanceSpot); - BeginMultiPurposeLocator(sAdvanceSpot, pSoldier->pathing.bLevel, FALSE); + BeginMultiPurposeLocator(sAdvanceSpot, pSoldier->pathing.bLevel, FALSE); // For AI debugging return AI_ACTION_GET_CLOSER; } @@ -21500,7 +21499,7 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) if (BestThrow.ubPossible) { DebugAI(AI_MSG_INFO, pSoldier, String("prepare throw at spot %d level %d aimtime %d", BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime)); - ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d status black advance to cover, throw smoke at target grid %d level %d aimtime %d", pSoldier->ubID.i, BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d BLACK advance to cover, throw smoke at target grid %d level %d aimtime %d", pSoldier->ubID.i, BestThrow.sTarget, BestThrow.bTargetLevel, BestThrow.ubAimTime); // if necessary, swap the usItem from holster into the hand position if (BestThrow.bWeaponIn != HANDPOS) @@ -21569,7 +21568,7 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) { DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "DecideActionBlack: can't hit so screw the attack"); DebugAI(AI_MSG_INFO, pSoldier, String("can't hit, screw the attack")); - ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d status black can't hit with chance %d, forget attack and allow cover check", pSoldier->ubID.i, BestAttack.ubChanceToReallyHit); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d BLACK can't hit with chance %d, forget attack and allow cover check", pSoldier->ubID.i, BestAttack.ubChanceToReallyHit); // screw the attack! ubBestAttackAction = AI_ACTION_NONE; } @@ -21596,7 +21595,7 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) if ( sBestCover != NOWHERE ) { DebugAI(AI_MSG_INFO, pSoldier, String("Found cover spot %d percent better %d movement mode %d", sBestCover, iCoverPercentBetter, DetermineMovementMode(pSoldier, AI_ACTION_TAKE_COVER))); - ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d Found cover spot %d percent better %d movement mode %d", pSoldier->ubID.i, sBestCover, iCoverPercentBetter, DetermineMovementMode(pSoldier, AI_ACTION_TAKE_COVER)); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"AI %d Found cover spot %d percent better %d movement mode %d", pSoldier->ubID.i, sBestCover, iCoverPercentBetter, DetermineMovementMode(pSoldier, AI_ACTION_TAKE_COVER)); } } From 75355f6cac389c45fb26f48922e0be02057074c6 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:26:48 +0200 Subject: [PATCH 70/80] Set DangerousSpot to const --- TacticalAI/DecideAction.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 06fa7fc16..15f7188d1 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -20542,12 +20542,8 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) const BOOLEAN fAnyCover = AnyCoverAtSpot(pSoldier, pSoldier->sGridNo); //const BOOLEAN fDangerousSpot = DangerousSpot(pSoldier); //const BOOLEAN fSafeSpot = SafeSpot(pSoldier); + const bool fDangerousSpot = (!fProneSightCover || (pSoldier->aiData.bUnderFire && !fAnyCover)); - BOOLEAN fDangerousSpot = FALSE; - if ( !fProneSightCover || (pSoldier->aiData.bUnderFire && !fAnyCover) ) - { - fDangerousSpot = TRUE; - } DebugAI(AI_MSG_INFO, pSoldier, String("prone sight cover %d", fProneSightCover), gLogDecideActionBlack); DebugAI(AI_MSG_INFO, pSoldier, String("any cover %d", fAnyCover), gLogDecideActionBlack); From b50ed93f3993b9a2e111d807698786fefa574c98 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:27:18 +0200 Subject: [PATCH 71/80] Remove armed vehicle check from soldier AI --- TacticalAI/DecideAction.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 15f7188d1..888e36662 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -20220,10 +20220,6 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) if (pSoldier->aiData.bAttitude == DEFENSIVE) iChance += 25; - if (ARMED_VEHICLE(pSoldier)) - { - iChance += 50; - } if ((INT16)PreRandom(100) < iChance && pSoldier->InternalIsValidStance(ubOpponentDir, gAnimControl[pSoldier->usAnimState].ubEndHeight)) { From 1bead3a0fe8e49a490c6ba56b447ecddb1703cf7 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:27:53 +0200 Subject: [PATCH 72/80] Improve readability --- TacticalAI/DecideAction.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 888e36662..579dbaaa2 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -18480,16 +18480,18 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) if (BestThrow.ubPossible) { DebugAI(AI_MSG_INFO, pSoldier, String("throw possible"), gLogDecideActionRed); + + const auto item = pSoldier->inv[BestThrow.bWeaponIn].usItem; + // sevenfm: allow using mortars, grenade launchers, flares and grenades in RED state - if (ItemIsMortar(pSoldier->inv[BestThrow.bWeaponIn].usItem) || - //Item[pSoldier->inv[ BestThrow.bWeaponIn ].usItem].cannon || - ItemIsRocketLauncher(pSoldier->inv[BestThrow.bWeaponIn].usItem) || - ItemIsGrenadeLauncher(pSoldier->inv[BestThrow.bWeaponIn].usItem) || - ItemIsFlare(pSoldier->inv[BestThrow.bWeaponIn].usItem) || - Item[pSoldier->inv[BestThrow.bWeaponIn].usItem].usItemClass & IC_GRENADE) + if (ItemIsMortar(item) || + ItemIsRocketLauncher(item) || + ItemIsGrenadeLauncher(item) || + ItemIsFlare(item) || + Item[item].usItemClass & IC_GRENADE) { // if firing mortar make sure we have room - if (ItemIsMortar(pSoldier->inv[BestThrow.bWeaponIn].usItem)) + if (ItemIsMortar(item)) { DebugAI(AI_MSG_INFO, pSoldier, String("using mortar, check room to deploy"), gLogDecideActionRed); ubOpponentDir = AIDirection(pSoldier->sGridNo, BestThrow.sTarget); From 737cd848ce784d342e0e48c89c0ef86bc874ed6f Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:28:37 +0200 Subject: [PATCH 73/80] Make water and gas checks const --- TacticalAI/DecideAction.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 579dbaaa2..dfaca9b6f 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -18414,13 +18414,10 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) // determine if we happen to be in water (in which case we're in BIG trouble!) - INT8 bInWater = Water(pSoldier->sGridNo, pSoldier->pathing.bLevel); - INT8 bInDeepWater = DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel); + const bool bInWater = Water(pSoldier->sGridNo, pSoldier->pathing.bLevel); + const bool bInDeepWater = DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel); + const bool bInGas = DecideActionWearGasmask(pSoldier); - //////////////////////////////////////////////////////////////////////////// - // WHEN LEFT IN GAS, WEAR GAS MASK IF AVAILABLE AND NOT WORN - //////////////////////////////////////////////////////////////////////////// - INT8 bInGas = DecideActionWearGasmask(pSoldier); //////////////////////////////////////////////////////////////////////////// // WHEN IN GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND From 3d7d7cc88acf9d5597ac5100772681ced88d56f2 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:31:31 +0200 Subject: [PATCH 74/80] Reorganize stuff to improve readability --- TacticalAI/DecideAction.cpp | 116 ++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 63 deletions(-) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index dfaca9b6f..616e67c1e 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -18301,34 +18301,25 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) DebugAI(AI_MSG_START, pSoldier, String("[Red Soldier]"), gLogDecideActionRed); LogDecideInfo(pSoldier, gLogDecideActionRed); - auto decision = AI_ACTION_INVALID; + // if we have absolutely no action points, we can't do a thing under RED! + if ( pSoldier->bActionPoints <= 0 ) //Action points can be negative + { + pSoldier->aiData.usActionData = NOWHERE; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + return(AI_ACTION_NONE); + } - INT32 iDummy; - INT32 iChance; - INT32 sClosestDisturbance = NOWHERE, sCheckGridNo; - INT32 sDistVisible; - UINT8 ubCanMove, ubOpponentDir; - INT8 bSeekPts = 0, bHelpPts = 0, bHidePts = 0, bWatchPts = 0; - INT8 bHighestWatchLoc; - ATTACKTYPE BestThrow, BestShot; + //////////////////////////////////////////////////////////////////////////// + // Prepare Data + //////////////////////////////////////////////////////////////////////////// BOOLEAN fClimb; + INT8 bSeekPts = 0, bHelpPts = 0, bHidePts = 0, bWatchPts = 0; - - // sevenfm: disable stealth mode pSoldier->bStealthMode = FALSE; - // disable reverse movement mode - pSoldier->bReverse = FALSE; - // sevenfm: initialize data + pSoldier->bReverse = FALSE; // disable reverse movement mode pSoldier->bWeaponMode = WM_NORMAL; - // if we have absolutely no action points, we can't do a thing under RED! - if (pSoldier->bActionPoints <= 0) //Action points can be negative - { - pSoldier->aiData.usActionData = NOWHERE; - pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; - return(AI_ACTION_NONE); - } // sevenfm: find closest opponent INT32 sOpponentGridNo; @@ -18337,49 +18328,45 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) const INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel, NULL, &distanceToOpponent); DebugAI(AI_MSG_INFO, pSoldier, String("sClosestOpponent %d", sClosestOpponent), gLogDecideActionRed); - BOOLEAN fCanBeSeen = FALSE; - if (!SightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE)) - { - fCanBeSeen = TRUE; - DebugAI(AI_MSG_INFO, pSoldier, String("can be seen"), gLogDecideActionRed); - } - BOOLEAN fProneSightCover = ProneSightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE); - BOOLEAN fAnyCover = AnyCoverAtSpot(pSoldier, pSoldier->sGridNo); + const bool fCanBeSeen = !SightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE); + const bool fProneSightCover = ProneSightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE); + const bool fAnyCover = AnyCoverAtSpot(pSoldier, pSoldier->sGridNo); + const bool fDangerousSpot = (!fProneSightCover || pSoldier->aiData.bUnderFire); + + DebugAI(AI_MSG_INFO, pSoldier, String("can be seen %d", fCanBeSeen), gLogDecideActionRed); DebugAI(AI_MSG_INFO, pSoldier, String("prone sight cover %d", fProneSightCover), gLogDecideActionRed); DebugAI(AI_MSG_INFO, pSoldier, String("any cover %d", fAnyCover), gLogDecideActionRed); - BOOLEAN fDangerousSpot = FALSE; - if (!fProneSightCover || pSoldier->aiData.bUnderFire) - { - fDangerousSpot = TRUE; - } + // Do commonly used checks in advance // can this guy move to any of the neighbouring squares ? (sets TRUE/FALSE) - ubCanMove = (pSoldier->bActionPoints >= MinPtsToMove(pSoldier)); + const bool ubCanMove = (pSoldier->bActionPoints >= MinPtsToMove(pSoldier)); + const bool canFunction = (pSoldier->stats.bLife >= OKLIFE && !pSoldier->bCollapsed && !pSoldier->bBreathCollapsed); + + + //////////////////////////////////////////////////////////////////////////// + // Start evaluating decisions + //////////////////////////////////////////////////////////////////////////// + auto decision = AI_ACTION_INVALID; + // sevenfm: before deciding anything, stop cowering - if (ubCanMove && - pSoldier->stats.bLife >= OKLIFE && - !pSoldier->bCollapsed && - !pSoldier->bBreathCollapsed && - pSoldier->IsCowering()) + if (ubCanMove && canFunction && pSoldier->IsCowering()) { DebugAI(AI_MSG_INFO, pSoldier, String("Stop cowering"), gLogDecideActionRed); return AI_ACTION_STOP_COWERING; } + // sevenfm: stop giving aid - if (pSoldier->bActionPoints > 0 && - pSoldier->stats.bLife >= OKLIFE && - !pSoldier->bCollapsed && - !pSoldier->bBreathCollapsed && - pSoldier->IsGivingAid()) + if (pSoldier->bActionPoints > 0 && canFunction && pSoldier->IsGivingAid()) { DebugAI(AI_MSG_INFO, pSoldier, String("Stop giving aid"), gLogDecideActionRed); return AI_ACTION_STOP_MEDIC; } + // if we're an alerted enemy, and there are panic bombs or a trigger around if ((!PTR_CIVILIAN || pSoldier->ubProfile == WARDEN) && ((gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition || (pSoldier->ubID == gTacticalStatus.ubTheChosenOne) || (pSoldier->ubProfile == WARDEN)) && (gTacticalStatus.fPanicFlags & (PANIC_BOMBS_HERE | PANIC_TRIGGERS_HERE)))) @@ -18422,6 +18409,7 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) //////////////////////////////////////////////////////////////////////////// // WHEN IN GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND //////////////////////////////////////////////////////////////////////////// + // when in deep water, move to closest opponent DebugAI(AI_MSG_TOPIC, pSoldier, String("[Decide action if stuck in water or gas]"), gLogDecideActionRed); if (ubCanMove && bInDeepWater && !pSoldier->aiData.bNeutral && pSoldier->aiData.bOrders == SEEKENEMY) @@ -18457,6 +18445,7 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) //////////////////////////////////////////////////////////////////////// // IF POSSIBLE, FIRE LONG RANGE WEAPONS AT TARGETS REPORTED BY RADIO //////////////////////////////////////////////////////////////////////// + ATTACKTYPE BestThrow, BestShot; // can't do this in realtime, because the player could be shooting a gun or whatever at the same time! if (gfTurnBasedAI && @@ -18468,8 +18457,8 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) { BestThrow.ubPossible = FALSE; // by default, assume Throwing isn't possible DebugAI(AI_MSG_TOPIC, pSoldier, String("[CheckIfTossPossible]"), gLogDecideActionRed); - CheckIfTossPossible(pSoldier, &BestThrow); + CheckIfTossPossible(pSoldier, &BestThrow); //////////////////////////////////////////////////////////////////////// // CHECK IF THROWING A GRENADE OR USING A LAUNCHER/MORTAR AGAINST ENEMY IS POSSIBLE @@ -18491,10 +18480,10 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) if (ItemIsMortar(item)) { DebugAI(AI_MSG_INFO, pSoldier, String("using mortar, check room to deploy"), gLogDecideActionRed); - ubOpponentDir = AIDirection(pSoldier->sGridNo, BestThrow.sTarget); + UINT8 ubOpponentDir = AIDirection(pSoldier->sGridNo, BestThrow.sTarget); // Get new gridno! - sCheckGridNo = NewGridNo(pSoldier->sGridNo, DirectionInc(ubOpponentDir)); + INT32 sCheckGridNo = NewGridNo(pSoldier->sGridNo, DirectionInc(ubOpponentDir)); if (!OKFallDirection(pSoldier, sCheckGridNo, pSoldier->pathing.bLevel, ubOpponentDir, pSoldier->usAnimState)) { @@ -19145,7 +19134,6 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) (pSoldier->CheckInitialAP() || !fAnyCover || pSoldier->aiData.bUnderFire)) { DebugAI(AI_MSG_TOPIC, pSoldier, String("[retreat]"), gLogDecideActionRed); - DebugAI(AI_MSG_TOPIC, pSoldier, String("search for retreat spot"), gLogDecideActionRed); INT32 sRetreatSpot = FindRetreatSpot(pSoldier); if (!TileIsOutOfBounds(sRetreatSpot)) @@ -19335,7 +19323,7 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) //////////////////////////////////////////////////////////////////////////// // get the location of the closest reachable opponent - sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb); + INT32 sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb); DebugMsg(TOPIC_JA2, DBG_LEVEL_3, "decideactionred: check to continue flanking"); // continue flanking @@ -19518,7 +19506,7 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) DebugAI(AI_MSG_INFO, pSoldier, String("found path to %d, path size %d ", sClosestDisturbance, pSoldier->pathing.usPathDataSize), gLogDecideActionRed); DebugAI(AI_MSG_INFO, pSoldier, String("check path for seen spots"), gLogDecideActionRed); - sCheckGridNo = pSoldier->sGridNo; + INT32 sCheckGridNo = pSoldier->sGridNo; for (sLoop = pSoldier->pathing.usPathIndex; sLoop < pSoldier->pathing.usPathDataSize; sLoop++) { @@ -19896,12 +19884,12 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) { DebugAI(AI_MSG_INFO, pSoldier, String("[watch]"), gLogDecideActionRed); // take a look at our highest watch point... if it's still visible, turn to face it and then wait - bHighestWatchLoc = GetHighestVisibleWatchedLoc(pSoldier->ubID); + INT8 bHighestWatchLoc = GetHighestVisibleWatchedLoc(pSoldier->ubID); if (bHighestWatchLoc != -1) { // see if we need turn to face that location - ubOpponentDir = AIDirection(pSoldier->sGridNo, gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc]); + UINT8 ubOpponentDir = AIDirection(pSoldier->sGridNo, gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc]); DebugAI(AI_MSG_INFO, pSoldier, String("Highest watch location: [%d] %d %d watch dir: %d", bHighestWatchLoc, gsWatchedLoc[pSoldier->ubID][bHighestWatchLoc], gbWatchedLocLevel[pSoldier->ubID][bHighestWatchLoc], ubOpponentDir), gLogDecideActionRed); // consider at least crouching @@ -20062,6 +20050,7 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) uiStartTime = GetJA2Clock(); #endif + INT32 iDummy; pSoldier->aiData.usActionData = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iDummy); #ifdef AI_TIMING_TESTS uiEndTime = GetJA2Clock(); @@ -20201,16 +20190,17 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) { DebugAI(AI_MSG_TOPIC, pSoldier, String("[Look around towards enemy]"), gLogDecideActionRed); // determine direction from this soldier to the closest opponent - ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestOpponent); + UINT8 ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestOpponent); // if soldier is not already facing in that direction, // and the opponent is close enough that he could possibly be seen // note, have to change this to use the level returned from ClosestKnownOpponent - sDistVisible = pSoldier->GetMaxDistanceVisible(sClosestOpponent, 0, CALC_FROM_ALL_DIRS)*CELL_X_SIZE; + INT32 sDistVisible = pSoldier->GetMaxDistanceVisible(sClosestOpponent, 0, CALC_FROM_ALL_DIRS)*CELL_X_SIZE; if ((pSoldier->ubDirection != ubOpponentDir) && (distanceToOpponent <= sDistVisible)) { // set base chance according to orders + INT32 iChance = 0; if ((pSoldier->aiData.bOrders == STATIONARY) || (pSoldier->aiData.bOrders == ONGUARD)) iChance = 50; else // all other orders @@ -20381,7 +20371,7 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) if (!gfTurnBasedAI || (GetAPsToReadyWeapon(pSoldier, READY_RIFLE_CROUCH) + GetAPsToChangeStance(pSoldier, ANIM_CROUCH)) <= pSoldier->bActionPoints) { // determine direction from this soldier to the closest opponent - ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestOpponent); + UINT8 ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestOpponent); if (!WeaponReady(pSoldier) && pSoldier->ubDirection == ubOpponentDir && @@ -20414,11 +20404,11 @@ INT8 DecideActionRedSoldier(SOLDIERTYPE* pSoldier) if (pSoldier->aiData.bUnderFire && pSoldier->bActionPoints >= (pSoldier->bInitialActionPoints - GetAPsToLook(pSoldier)) && IsValidStance(pSoldier, ANIM_PRONE)) { DebugAI(AI_MSG_TOPIC, pSoldier, String("[Under fire, go prone]"), gLogDecideActionRed); - sClosestDisturbance = MostImportantNoiseHeard(pSoldier, NULL, NULL, NULL); + INT32 sClosestDisturbance = MostImportantNoiseHeard(pSoldier, NULL, NULL, NULL); if (!TileIsOutOfBounds(sClosestDisturbance)) { - ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestDisturbance); + UINT8 ubOpponentDir = GetDirectionFromCenterCellXYGridNo(pSoldier->sGridNo, sClosestDisturbance); if (pSoldier->ubDirection != ubOpponentDir) { if (!gfTurnBasedAI || GetAPsToLook(pSoldier) <= pSoldier->bActionPoints) @@ -20508,7 +20498,6 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) { DebugAI(AI_MSG_START, pSoldier, String("[Black Soldier]")); LogDecideInfo(pSoldier); - auto decision = AI_ACTION_INVALID; // if we have absolutely no action points, we can't do a thing under BLACK! if ( pSoldier->bActionPoints <= 0 || pSoldier->IsUnconscious() ) @@ -20519,7 +20508,7 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) } //////////////////////////////////////////////////////////////////////////// - // Prepare data + // Prepare Data //////////////////////////////////////////////////////////////////////////// pSoldier->bStealthMode = FALSE; // sevenfm: disable stealth mode pSoldier->bReverse = FALSE; // disable reverse movement mode @@ -20553,6 +20542,7 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) //////////////////////////////////////////////////////////////////////////// // Start evaluating decisions //////////////////////////////////////////////////////////////////////////// + auto decision = AI_ACTION_INVALID; // sevenfm: stop flanking when we see enemy if ( AICheckIsFlanking(pSoldier) ) @@ -20612,9 +20602,9 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) } // determine if we happen to be in water (in which case we're in BIG trouble!) - INT8 bInWater = Water(pSoldier->sGridNo, pSoldier->pathing.bLevel); - INT8 bInDeepWater = WaterTooDeepForAttacks(pSoldier->sGridNo, pSoldier->pathing.bLevel); - INT8 bInGas = DecideActionWearGasmask(pSoldier); + const bool bInWater = Water(pSoldier->sGridNo, pSoldier->pathing.bLevel); + const bool bInDeepWater = WaterTooDeepForAttacks(pSoldier->sGridNo, pSoldier->pathing.bLevel); + const bool bInGas = DecideActionWearGasmask(pSoldier); pSoldier->aiData.bAIMorale = CalcMorale(pSoldier); From 9c0fb0b6da6ed4810f5396b5972510764db821ef Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:32:41 +0200 Subject: [PATCH 75/80] Add tactical range defines --- TacticalAI/ai.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index a450cd240..fc10e1424 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -433,5 +433,7 @@ INT8 KnownPublicLevel(UINT8 bTeam, SoldierID ubOpponentID); #define TACTICAL_RANGE (gGameExternalOptions.ubStraightSightRange * STRAIGHT_RATIO * 2) #define TACTICAL_RANGE_CELL_COORDS TACTICAL_RANGE*CELL_X_SIZE #define BOMB_DETECTION_RANGE (TACTICAL_RANGE / 4) +#define TACTICAL_RANGE_CLOSE (TACTICAL_RANGE / 4) +#define TACTICAL_RANGE_VERYCLOSE (TACTICAL_RANGE / 6) #endif From 91a1059f87190ff233ac9f4bded2fcfb19b908e1 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:47:34 +0200 Subject: [PATCH 76/80] Migrate features from +AI Possibly wear gasmask if soldier sees tear or mustard gas --- TacticalAI/AIUtils.cpp | 55 +++++++++++++++++++++++++++++++++++++ TacticalAI/DecideAction.cpp | 14 ++++++++++ TacticalAI/ai.h | 1 + 3 files changed, 70 insertions(+) diff --git a/TacticalAI/AIUtils.cpp b/TacticalAI/AIUtils.cpp index 2ea42f281..d1160f681 100644 --- a/TacticalAI/AIUtils.cpp +++ b/TacticalAI/AIUtils.cpp @@ -4619,6 +4619,61 @@ UINT8 RedSmokeDanger(INT32 sGridNo, INT8 bLevel) return ubDangerPercent; } +BOOLEAN FindClosestVisibleSmoke(SOLDIERTYPE* pSoldier, INT32& sSpot, INT8& bLevel, BOOLEAN fOnlyGas) +{ + CHECKF(pSoldier); + + INT32 sDist; + INT32 sClosestDist = INT32_MAX; + INT32 sCheckSpot; + INT8 bCheckLevel; + + sSpot = NOWHERE; + bLevel = 0; + + //loop through all smoke effects and find closest visible + for ( UINT32 uiCnt = 0; uiCnt < guiNumSmokeEffects; uiCnt++ ) + { + if ( gSmokeEffectData[uiCnt].fAllocated && + !TileIsOutOfBounds(gSmokeEffectData[uiCnt].sGridNo) ) + { + // ignore smoke if not dangerous + if ( fOnlyGas && + gSmokeEffectData[uiCnt].bType != TEARGAS_SMOKE_EFFECT && + gSmokeEffectData[uiCnt].bType != MUSTARDGAS_SMOKE_EFFECT && + gSmokeEffectData[uiCnt].bType != CREATURE_SMOKE_EFFECT ) + { + continue; + } + + sCheckSpot = gSmokeEffectData[uiCnt].sGridNo; + + if ( gSmokeEffectData[uiCnt].bFlags & SMOKE_EFFECT_ON_ROOF ) + bCheckLevel = 1; + else + bCheckLevel = 0; + + sDist = PythSpacesAway(sCheckSpot, pSoldier->sGridNo); + + if ( sDist < DAY_VISION_RANGE && + SoldierToVirtualSoldierLineOfSightTest(pSoldier, sCheckSpot, bCheckLevel, ANIM_PRONE, 0, CALC_FROM_ALL_DIRS) && + (sSpot == NOWHERE || sDist < sClosestDist) ) + { + sClosestDist = sDist; + sSpot = sCheckSpot; + bLevel = bCheckLevel; + } + } + } + + if ( !TileIsOutOfBounds(sSpot) ) + { + return TRUE; + } + + return FALSE; +} + // check if artillery strike was ordered by any team BOOLEAN CheckArtilleryStrike(void) { diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 616e67c1e..5a6759add 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -1,3 +1,4 @@ +//#pragma optimize("", off) #include "ai.h" #include "AIInternals.h" #include "Isometric Utils.h" @@ -10730,6 +10731,19 @@ INT8 DecideActionWearGasmask(SOLDIERTYPE *pSoldier) //Only put mask on in gas if (bInGas && WearGasMaskIfAvailable(pSoldier)) { bInGas = InGasOrSmoke(pSoldier, pSoldier->sGridNo); } + // Chance to wear gas mask if soldier sees gas nearby + INT32 sSmokeSpot = NOWHERE; + INT8 bSmokeLevel = 0; + + if ( SoldierAI(pSoldier) && + pSoldier->CheckInitialAP() && + !DoesSoldierWearGasMask(pSoldier) && + FindClosestVisibleSmoke(pSoldier, sSmokeSpot, bSmokeLevel, TRUE) && + Chance(30 + SoldierDifficultyLevel(pSoldier) * 10) ) + { + WearGasMaskIfAvailable(pSoldier); + } + return bInGas; } diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index fc10e1424..e6f16d455 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -299,6 +299,7 @@ SoldierID GetClosestMedicSoldierID( SOLDIERTYPE * pSoldier, INT16 aRange, UINT8 // sevenfm: BOOLEAN NightLight(void); BOOLEAN DuskLight(void); +BOOLEAN FindClosestVisibleSmoke(SOLDIERTYPE* pSoldier, INT32& sSpot, INT8& bLevel, BOOLEAN fOnlyGas); BOOLEAN InSmokeNearby(INT32 sGridNo, INT8 bLevel); INT16 MaxNormalVisionDistance( void ); UINT8 MinFlankDirections( SOLDIERTYPE *pSoldier ); From 41945842d531dc10cce644f463fea2c23d803536 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:06:26 +0200 Subject: [PATCH 77/80] Migrate features from +AI Create Scuba fins when in deep water for elites --- TacticalAI/DecideAction.cpp | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 5a6759add..d6bdacbe6 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -20667,6 +20667,56 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) } + //////////////////////////////////////////////////////////////////////////// + // WHEN IN DEEP WATER, CREATE SCUBA FINS + //////////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_TOPIC, pSoldier, String("[when in deep water, create scuba fins]")); + + if ( bInDeepWater && + ubCanMove && + pSoldier->ubSoldierClass == SOLDIER_CLASS_ELITE && // Only elites get to play with tacticool toys + pSoldier->IsFlanking() && + pSoldier->CheckInitialAP() && + Chance(25) && + 20 * SoldierDifficultyLevel(pSoldier) >= 100 - gStrategicStatus.ubHighestProgress && + !FindNotDeepWaterNearby(pSoldier->sGridNo, pSoldier->pathing.bLevel) && + !pSoldier->inv[LEGPOS].exists() ) + { + // create scuba fins and place in the inventory + UINT16 usItem; + if ( GetFirstItemWithFlag(&usItem, SCUBA_FINS) ) + { + OBJECTTYPE newobj; + CreateItem(usItem, 80 + Random(20), &newobj); + newobj.fFlags |= OBJECT_UNDROPPABLE; + + // try to place item in inventory + int slot = LEGPOS; + if ( TryToPlaceInSlot(pSoldier, &newobj, false, slot, slot) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("successfully created and placed scuba fins in inventory")); + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // WHEN NOT IN WATER, DROP SCUBA FINS + //////////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_TOPIC, pSoldier, String("[when not in water, drop scuba fins]")); + + if ( !bInWater && + pSoldier->CheckInitialAP() && + pSoldier->inv[LEGPOS].exists() && + HasItemFlag(pSoldier->inv[LEGPOS].usItem, SCUBA_FINS) && + IsActionAffordable(pSoldier, AI_ACTION_DROP_ITEM) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("found scuba fins in LEGPOS, drop to the ground")); + pSoldier->inv[LEGPOS].fFlags |= OBJECT_AI_UNUSABLE; + pSoldier->aiData.usActionData = LEGPOS; + + return AI_ACTION_DROP_ITEM; + } + //////////////////////////////////////////////////////////////////////////// // STUCK IN WATER OR GAS, NO COVER, GO TO NEAREST SPOT OF UNGASSED LAND From bd9cbd994496184c340e265d561f4861328b3358 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:19:07 +0200 Subject: [PATCH 78/80] Migrate +AI features * Drink canteen * Jump through window * Use explosives * Use wirecutters --- Tactical/Food.cpp | 23 +++ Tactical/Food.h | 1 + Tactical/Items.cpp | 59 ++++++ Tactical/Items.h | 5 + Tactical/Overhead.cpp | 38 +++- Tactical/Overhead.h | 4 +- Tactical/Soldier Control.cpp | 89 +++++++++ Tactical/Soldier Control.h | 3 + Tactical/Weapons.cpp | 12 +- Tactical/Weapons.h | 2 +- TacticalAI/AIMain.cpp | 93 +++++++++- TacticalAI/AIUtils.cpp | 231 ++++++++++++++++++++++++ TacticalAI/DecideAction.cpp | 338 ++++++++++++++++++++++++++++++++++- TacticalAI/ai.h | 12 ++ 14 files changed, 896 insertions(+), 14 deletions(-) diff --git a/Tactical/Food.cpp b/Tactical/Food.cpp index 5feeb57d4..0fc5a5a89 100644 --- a/Tactical/Food.cpp +++ b/Tactical/Food.cpp @@ -758,6 +758,29 @@ void EatFromInventory( SOLDIERTYPE *pSoldier, BOOLEAN fcanteensonly ) } } +void DrinkFromInventory(SOLDIERTYPE* pSoldier) +{ + if ( !pSoldier ) + return; + + INT8 invsize = pSoldier->inv.size(); + + for ( INT8 bLoop = 0; bLoop < invsize; ++bLoop ) + { + if ( pSoldier->inv[bLoop].exists() && ItemIsCanteen(pSoldier->inv[bLoop].usItem) ) + { + OBJECTTYPE* pObj = &(pSoldier->inv[bLoop]); + + if ( pObj && TotalPoints(pObj) > 1 ) + { + //ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, Message[STR_DRINKS], pSoldier->GetName()); + ApplyConsumable(pSoldier, pObj, false, true); + return; + } + } + } +} + void HourlyFoodUpdate( void ) { SoldierID bMercID, bLastTeamID; diff --git a/Tactical/Food.h b/Tactical/Food.h index c0181e02c..e876aae2c 100644 --- a/Tactical/Food.h +++ b/Tactical/Food.h @@ -108,5 +108,6 @@ void SoldierAutoFillCanteens(SOLDIERTYPE *pSoldier); BOOLEAN HasFoodInInventory( SOLDIERTYPE *pSoldier, BOOLEAN fCheckFood, BOOLEAN fCheckDrink ); void DrinkFromWaterTap( SOLDIERTYPE *pSoldier ); +void DrinkFromInventory(SOLDIERTYPE* pSoldier); #endif diff --git a/Tactical/Items.cpp b/Tactical/Items.cpp index c8e802e87..e3d7031c8 100644 --- a/Tactical/Items.cpp +++ b/Tactical/Items.cpp @@ -12426,6 +12426,51 @@ INT8 FindCamoKit( SOLDIERTYPE * pSoldier ) return( NO_SLOT ); } +INT8 FindCanteen(SOLDIERTYPE* pSoldier) +{ + const INT8 invsize = pSoldier->inv.size(); + for ( INT8 bLoop = 0; bLoop < invsize; ++bLoop ) + { + if ( pSoldier->inv[bLoop].exists() && ItemIsCanteen(pSoldier->inv[bLoop].usItem) ) + { + OBJECTTYPE* pObj = &(pSoldier->inv[bLoop]); + if ( pObj && TotalPoints(pObj) > 1 ) + { + return(bLoop); + } + } + } + return(NO_SLOT); +} + +INT8 FindWirecutters(SOLDIERTYPE* pSoldier) +{ + const INT8 invsize = pSoldier->inv.size(); + for ( INT8 bLoop = 0; bLoop < invsize; ++bLoop ) + { + if ( pSoldier->inv[bLoop].exists() && ItemIsWirecutters(pSoldier->inv[bLoop].usItem) ) + { + return(bLoop); + } + } + + return NO_SLOT; +} + +INT8 FindTNT(SOLDIERTYPE* pSoldier) +{ + const INT8 invsize = pSoldier->inv.size(); + for ( INT8 bLoop = 0; bLoop < invsize; ++bLoop ) + { + if ( pSoldier->inv[bLoop].exists() && ItemIsTNT(pSoldier->inv[bLoop].usItem) ) + { + return(bLoop); + } + } + + return NO_SLOT; +} + //JMich_SkillModifiers: Adding a function to see if we have an item with disarm bonus INT8 FindDisarmKit( SOLDIERTYPE * pSoldier ) { @@ -16045,6 +16090,20 @@ BOOLEAN FindAttachmentRange(UINT16 usAttachment, UINT32* pStartIndex, UINT32* pE return result; } + +BOOLEAN ItemIsTNT(UINT16 usItem) +{ + if ( Item[usItem].usItemClass == IC_BOMB && + !ItemIsMine(usItem) && + !ItemIsTripwire(usItem) && + Explosive[Item[usItem].ubClassIndex].ubType == EXPLOSV_NORMAL && + Explosive[Item[usItem].ubClassIndex].ubDamage > 40 && + GetLauncherFromLaunchable(usItem) == NOTHING ) + return TRUE; + + return FALSE; +} + //////////////////////////////////// // Item flagmask utility functions // Just to improve readability diff --git a/Tactical/Items.h b/Tactical/Items.h index 05060e955..0f18cc185 100644 --- a/Tactical/Items.h +++ b/Tactical/Items.h @@ -249,6 +249,7 @@ BOOLEAN ItemIsOnlyInDisease(UINT16 usItem); BOOLEAN ItemProvidesRobotCamo(UINT16 usItem); BOOLEAN ItemProvidesRobotNightvision(UINT16 usItem); BOOLEAN ItemProvidesRobotLaserBonus(UINT16 usItem); +BOOLEAN ItemIsTNT(UINT16 usItem); //Existing functions without header def's, added them here, just incase I'll need to call //them from the editor. @@ -479,9 +480,13 @@ INT8 FindFirstAidKit( SOLDIERTYPE * pSoldier ); INT8 FindDisarmKit( SOLDIERTYPE * pSoldier ); //JMich_SkillsModifiers: Added function to check for disarm bonus INT8 FindLocksmithKit( SOLDIERTYPE * pSoldier ); INT8 FindCamoKit( SOLDIERTYPE * pSoldier ); +INT8 FindCanteen(SOLDIERTYPE* pSoldier); INT8 FindWalkman( SOLDIERTYPE * pSoldier ); INT8 FindTrigger( SOLDIERTYPE * pSoldier ); INT8 FindRemoteControl( SOLDIERTYPE * pSoldier ); +INT8 FindWirecutters(SOLDIERTYPE* pSoldier); +INT8 FindTNT(SOLDIERTYPE* pSoldier); + INT16 GetWornCamo( SOLDIERTYPE * pSoldier ); INT16 GetCamoBonus( OBJECTTYPE * pObj ); INT16 GetWornStealth( SOLDIERTYPE * pSoldier ); diff --git a/Tactical/Overhead.cpp b/Tactical/Overhead.cpp index 6920158e4..a467443c2 100644 --- a/Tactical/Overhead.cpp +++ b/Tactical/Overhead.cpp @@ -5031,11 +5031,11 @@ BOOLEAN NewOKDestination( SOLDIERTYPE * pCurrSoldier, INT32 sGridNo, BOOLEAN fPe BOOLEAN fOKCheckStruct; // Allow civilians and NPCs with profile to go off screen, and also enemies if tactical retreat is enabled - auto destinationOffscreen = !(GridNoOnVisibleWorldTile(sGridNo)); - auto hasProfile = pCurrSoldier->ubProfile != NO_PROFILE; - auto isCivilian = pCurrSoldier->bTeam == CIV_TEAM; - auto isEnemy = pCurrSoldier->bTeam == ENEMY_TEAM; - auto retreatAllowed = gGameExternalOptions.fAITacticalRetreat == true; + const auto destinationOffscreen = !(GridNoOnVisibleWorldTile(sGridNo)); + const auto hasProfile = pCurrSoldier->ubProfile != NO_PROFILE; + const auto isCivilian = pCurrSoldier->bTeam == CIV_TEAM; + const auto isEnemy = pCurrSoldier->bTeam == ENEMY_TEAM; + const auto retreatAllowed = gGameExternalOptions.fAITacticalRetreat == TRUE; if (destinationOffscreen && !(isCivilian || hasProfile || (isEnemy && retreatAllowed))) { @@ -5400,6 +5400,34 @@ BOOLEAN TeamMemberNear( INT8 bTeam, INT32 sGridNo, INT32 iRange ) return(FALSE); } +BOOLEAN NotDeafTeamMemberNear(INT8 bTeam, INT32 sGridNo, INT32 iRange) +{ + for ( SoldierID pSoldier = gTacticalStatus.Team[bTeam].bFirstID; pSoldier <= gTacticalStatus.Team[bTeam].bLastID; ++pSoldier ) + { + if ( pSoldier->bActive && + pSoldier->bInSector && + pSoldier->stats.bLife >= OKLIFE && + pSoldier->bDeafenedCounter == 0 ) + { + if ( PythSpacesAway(pSoldier->sGridNo, sGridNo) <= iRange ) + { + return(TRUE); + } + } + } + + return(FALSE); +} + +BOOLEAN PlayerCanHearNoise(SOLDIERTYPE* pSoldier) +{ + if ( pSoldier && + (pSoldier->bVisible == TRUE || NotDeafTeamMemberNear(gbPlayerNum, pSoldier->sGridNo, TACTICAL_RANGE / 2) || NightLight() && !(gTacticalStatus.uiFlags & TURNBASED && gTacticalStatus.uiFlags & INCOMBAT) && NotDeafTeamMemberNear(gbPlayerNum, pSoldier->sGridNo, TACTICAL_RANGE)) ) + return TRUE; + + return FALSE; +} + INT32 FindAdjacentGridEx( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 *pubDirection, INT32 *psAdjustedGridNo, BOOLEAN fForceToPerson, BOOLEAN fDoor, bool allow_diagonal ) { // psAdjustedGridNo gets the original gridno or the new one if updated diff --git a/Tactical/Overhead.h b/Tactical/Overhead.h index 26b10674f..ea1cd1911 100644 --- a/Tactical/Overhead.h +++ b/Tactical/Overhead.h @@ -261,6 +261,9 @@ void SelectNextAvailSoldier( SOLDIERTYPE *pSoldier ); BOOLEAN TeamMemberNear(INT8 bTeam, INT32 sGridNo, INT32 iRange); BOOLEAN IsValidTargetMerc( SoldierID ubSoldierID ); +BOOLEAN NotDeafTeamMemberNear(INT8 bTeam, INT32 sGridNo, INT32 iRange); +BOOLEAN PlayerCanHearNoise(SOLDIERTYPE* pSoldier); + // FUNCTIONS FOR MANIPULATING MERC SLOTS - A LIST OF ALL ACTIVE MERCS INT32 GetFreeMercSlot( ); INT32 AddMercSlot( SOLDIERTYPE *pSoldier ); @@ -433,4 +436,3 @@ BOOLEAN IsFreeSlotAvailable( int aTeam ); void AttemptToCapturePlayerSoldiers(); #endif - diff --git a/Tactical/Soldier Control.cpp b/Tactical/Soldier Control.cpp index 778f6ae13..b8c0139ea 100644 --- a/Tactical/Soldier Control.cpp +++ b/Tactical/Soldier Control.cpp @@ -9700,6 +9700,85 @@ void SOLDIERTYPE::BeginSoldierClimbWallUp( void ) } //------------------------------------------------------------------------------------------ +void SOLDIERTYPE::BeginSoldierJumpWindowAI(void) +{ + DebugAI(AI_MSG_INFO, this, String("check if we can jump through window")); + + //UINT8 ubDirection = this->aiData.usActionData; + UINT8 ubDirection = this->ubDirection; + + INT32 sWindowGridNo = this->sGridNo; + if ( ubDirection == NORTH || ubDirection == WEST ) + sWindowGridNo = NewGridNo(this->sGridNo, (UINT16)DirectionInc((UINT8)ubDirection)); + + DebugAI(AI_MSG_INFO, this, String("sWindowGridNo %d direction %d", sWindowGridNo, ubDirection)); + + if (//CheckWindow(this->sGridNo, ubDirection, gGameExternalOptions.fCanJumpThroughClosedWindows) && + IsJumpableWindowPresentAtGridNo(sWindowGridNo, ubDirection, gGameExternalOptions.fCanJumpThroughClosedWindows) && + //FindWindowJumpDirection(this, this->sGridNo, bDirection, &bDirection) && + this->pathing.bLevel == 0 && + (ubDirection == NORTH || ubDirection == EAST || ubDirection == SOUTH || ubDirection == WEST) ) + { + // Flugente: if we are jumping through an intact window, smash it during our animation + if ( gGameExternalOptions.fCanJumpThroughClosedWindows ) + { + // is there really an intact window that we jump through? + if ( IsJumpableWindowPresentAtGridNo(sWindowGridNo, ubDirection, TRUE) && !IsJumpableWindowPresentAtGridNo(sWindowGridNo, ubDirection, FALSE) ) + { + STRUCTURE* pStructure = FindStructure(sWindowGridNo, STRUCTURE_WALLNWINDOW); + if ( pStructure && !(pStructure->fFlags & STRUCTURE_OPEN) ) + { + DebugAI(AI_MSG_INFO, this, String("jumping through closed window, damage soldier")); + // intact window found. Smash it! + WindowHit(sWindowGridNo, pStructure->usStructureID, (ubDirection == SOUTH || ubDirection == EAST), TRUE, PlayerCanHearNoise(this)); + + // we get a bit of damage for jumping through a window + this->SoldierTakeDamage(0, 2 + 2 * Random(4), 0, 1000, TAKE_DAMAGE_ELECTRICITY, NOBODY, sWindowGridNo, 0); + } + } + } + + this->sTempNewGridNo = NewGridNo(this->sGridNo, (UINT16)DirectionInc(ubDirection)); + this->flags.fDontChargeTurningAPs = TRUE; + EVENT_InternalSetSoldierDesiredDirection(this, ubDirection, FALSE, this->usAnimState); + this->flags.fTurningUntilDone = TRUE; + // ATE: Reset flag to go back to prone... + + // Flugente: In case an animation is missing (civilian bodytypes), we TELEPORT instead + if ( IsAnimationValidForBodyType(this, JUMPWINDOWS) == FALSE ) + { + DebugAI(AI_MSG_INFO, this, String("teleport soldier to %d", this->sTempNewGridNo)); + + // sevenfm: deduct APs for jumping + if ( UsingNewInventorySystem() && FindBackpackOnSoldier(this) != ITEM_NOT_FOUND ) + DeductPoints(this, GetAPsToJumpThroughWindows(this, TRUE), GetBPsToJumpThroughWindows(this, TRUE), SP_MOVEMENT_INTERRUPT); + else + DeductPoints(this, GetAPsToJumpThroughWindows(this, FALSE), GetBPsToJumpThroughWindows(this, FALSE), SP_MOVEMENT_INTERRUPT); + + TeleportSoldier(this, this->sTempNewGridNo, TRUE); + } + else + { + DebugAI(AI_MSG_INFO, this, String("start jumping")); + this->flags.bTurningFromPronePosition = TURNING_FROM_PRONE_OFF; + this->EVENT_InitNewSoldierAnim(JUMPWINDOWS, 0, FALSE); + //this->usPendingAnimation = JUMPWINDOWS; + //EndAIGuysTurn(this); + } + + // Flugente: should be fixed now, re-enable if not + // Flugente: if an AI guy, end turn (weird endless clock syndrome) + //if ( this->bTeam != OUR_TEAM ) + //EndAIGuysTurn( this); + } + else + { + ScreenMsg(FONT_LTRED, MSG_INTERFACE, L"[%d] %s cannot jump", this->ubID, this->GetName()); + DebugAI(AI_MSG_INFO, this, String("CancelAIAction: cannot jump")); + CancelAIAction(this, TRUE); + } +} + UINT32 SleepDartSuccumbChance( SOLDIERTYPE * pSoldier ) { UINT32 uiChance; @@ -26297,3 +26376,13 @@ void SOLDIERTYPE::InitializeExtraData(void) this->delayedDamageFunction = nullptr; } + +UINT8 SOLDIERTYPE::AnimHeight(void) const +{ + return gAnimControl[this->usAnimState].ubHeight; +} + +UINT8 SOLDIERTYPE::AnimEndHeight(void) const +{ + return gAnimControl[this->usAnimState].ubEndHeight; +} diff --git a/Tactical/Soldier Control.h b/Tactical/Soldier Control.h index 3bba03d35..bdb3190ae 100644 --- a/Tactical/Soldier Control.h +++ b/Tactical/Soldier Control.h @@ -1788,10 +1788,13 @@ class SOLDIERTYPE//last edited at version 102 void ChangeToFallbackAnimation( UINT8 fallBackDirection ); // sevenfm + void BeginSoldierJumpWindowAI(void); void BreakWindow(void); BOOLEAN CanBreakWindow(void); BOOLEAN CanStartDrag(void); void StartDrag(void); + UINT8 AnimHeight(void) const; + UINT8 AnimEndHeight(void) const; void UpdateRobotControllerGivenController( void ); void UpdateRobotControllerGivenRobot( void ); diff --git a/Tactical/Weapons.cpp b/Tactical/Weapons.cpp index a7702e0f6..0a39224bf 100644 --- a/Tactical/Weapons.cpp +++ b/Tactical/Weapons.cpp @@ -6089,7 +6089,7 @@ void StructureHit( INT32 iBullet, UINT16 usWeaponIndex, INT16 bWeaponStatus, Sol } } -void WindowHit( INT32 sGridNo, UINT16 usStructureID, BOOLEAN fBlowWindowSouth, BOOLEAN fLargeForce ) +void WindowHit( INT32 sGridNo, UINT16 usStructureID, BOOLEAN fBlowWindowSouth, BOOLEAN fLargeForce, BOOLEAN fSound) { STRUCTURE * pWallAndWindow; DB_STRUCTURE * pWallAndWindowInDB; @@ -6210,10 +6210,12 @@ void WindowHit( INT32 sGridNo, UINT16 usStructureID, BOOLEAN fBlowWindowSouth, B pNode = CreateAnimationTile( &AniParams ); //ddd window{ -CompileWorldMovementCosts(); -//ddd window} - PlayJA2Sample( GLASS_SHATTER1 + Random(2), RATE_11025, MIDVOLUME, 1, SoundDir( sGridNo ) ); - + CompileWorldMovementCosts(); + //ddd window} + if ( fSound ) + { + PlayJA2Sample(GLASS_SHATTER1 + Random(2), RATE_11025, MIDVOLUME, 1, SoundDir(sGridNo)); + } } diff --git a/Tactical/Weapons.h b/Tactical/Weapons.h index 8f6afb7ee..2d5d03002 100644 --- a/Tactical/Weapons.h +++ b/Tactical/Weapons.h @@ -456,7 +456,7 @@ INT16 ArmourVersusFirePercent( SOLDIERTYPE * pSoldier ); extern BOOLEAN FireWeapon( SOLDIERTYPE *pSoldier , INT32 sTargetGridNo ); extern void WeaponHit( SoldierID usSoldierID, UINT16 usWeaponIndex, INT16 sDamage, INT16 sBreathLoss, UINT16 usDirection, INT16 sXPos, INT16 sYPos, INT16 sZPos, INT16 sRange, SoldierID ubAttackerID, BOOLEAN fHit, UINT8 ubSpecial, UINT8 ubHitLocation ); extern void StructureHit( INT32 iBullet, UINT16 usWeaponIndex, INT16 bWeaponStatus, SoldierID ubAttackerID, UINT16 sXPos, INT16 sYPos, INT16 sZPos, UINT16 usStructureID, INT32 iImpact, BOOLEAN fStopped ); -extern void WindowHit( INT32 sGridNo, UINT16 usStructureID, BOOLEAN fBlowWindowSouth, BOOLEAN fLargeForce ); +void WindowHit(INT32 sGridNo, UINT16 usStructureID, BOOLEAN fBlowWindowSouth, BOOLEAN fLargeForce, BOOLEAN fSound = TRUE); // HEADROCK HAM 5.1: Moved to Bullets.h extern BOOLEAN InRange( SOLDIERTYPE *pSoldier, INT32 sGridNo ); extern void ShotMiss( SoldierID ubAttackerID, INT32 iBullet ); diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 8786656ee..d4935daad 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -49,6 +49,7 @@ #include "Soldier Functions.h" // added by SANDRO #include "Text.h" // sevenfm #include "english.h" // sevenfm: for ESC key +#include "Food.h" #include "connect.h" // needed to use the modularized tactical AI: @@ -2744,6 +2745,30 @@ INT8 ExecuteAction(SOLDIERTYPE *pSoldier) case AI_ACTION_JUMP_WINDOW: { pSoldier->BeginSoldierClimbWindow(); + pSoldier->BeginSoldierJumpWindowAI(); + if ( gfTurnBasedAI ) + { + //if (pSoldier->bActionPoints >= GetAPsToJumpThroughWindows(pSoldier, FALSE) + GetAPsToLook(pSoldier)) + if ( pSoldier->bActionPoints >= GetAPsToLook(pSoldier) ) + { + pSoldier->aiData.bNextAction = AI_ACTION_CHANGE_FACING; + INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); + if ( !TileIsOutOfBounds(sClosestOpponent) ) + pSoldier->aiData.usNextActionData = AIDirection(pSoldier->sGridNo, sClosestOpponent); + else + pSoldier->aiData.usNextActionData = PreRandom(8); + } + else + { + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + pSoldier->aiData.usNextActionData = 0; + } + } + else + { + pSoldier->aiData.bNextAction = AI_ACTION_WAIT; + pSoldier->aiData.usNextActionData = REALTIME_AI_DELAY / 10; + } ActionDone( pSoldier ); } break; @@ -2806,7 +2831,73 @@ INT8 ExecuteAction(SOLDIERTYPE *pSoldier) } break; - default: + case AI_ACTION_DRINK_CANTEEN: + DrinkFromInventory(pSoldier); + ActionDone(pSoldier); + break; + + case AI_ACTION_HANDLE_ITEM: + iRetCode = HandleItem(pSoldier, pSoldier->aiData.usActionData, pSoldier->pathing.bLevel, pSoldier->inv[HANDPOS].usItem, FALSE); + if ( iRetCode != ITEM_HANDLE_OK ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("CancelAIAction (AI_ACTION_HANDLE_ITEM): HandleItem error code %d", iRetCode)); + CancelAIAction(pSoldier, FORCE); + EndAIGuysTurn(pSoldier); + } + break; + + case AI_ACTION_PLANT_BOMB: + if ( pSoldier->inv[HANDPOS].exists() && + (Item[pSoldier->inv[HANDPOS].usItem].usItemClass & IC_BOMB) ) + { + OBJECTTYPE bombobj; + INT32 sSpot = pSoldier->sGridNo; + + if ( !TileIsOutOfBounds(sSpot) && + pSoldier->inv[HANDPOS].MoveThisObjectTo(bombobj, 1) == 0 ) + { + bombobj.fFlags |= OBJECT_ARMED_BOMB; + bombobj[0]->data.misc.bDetonatorType = BOMB_TIMED; + bombobj[0]->data.misc.usBombItem = bombobj.usItem; + //bombobj[0]->data.misc.ubBombOwner = pSoldier->ubID + 2; + bombobj[0]->data.misc.ubBombOwner = 1; + bombobj[0]->data.misc.bDelay = 1 + Random(2); + //pSoldier->inv[HANDPOS][0]->data.bTrap = EffectiveExplosive(pSoldier) / 20 + EffectiveExpLevel(pSoldier, TRUE) / 2; + pSoldier->inv[HANDPOS][0]->data.bTrap = 6 + SoldierDifficultyLevel(pSoldier); + AddItemToPool(sSpot, &bombobj, INVISIBLE, pSoldier->pathing.bLevel, WORLD_ITEM_ARMED_BOMB, 0); + NotifySoldiersToLookforItems(); + DeductPoints(pSoldier, APBPConstants[AP_INVENTORY_ARM] + APBPConstants[AP_DROP_BOMB], APBPConstants[BP_INVENTORY_ARM] + APBPConstants[BP_DROP_BOMB]); + if ( gAnimControl[pSoldier->usAnimState].ubHeight == ANIM_STAND ) + { + pSoldier->EVENT_InitNewSoldierAnim(DROP_ITEM, 0, FALSE); + } + else if ( gAnimControl[pSoldier->usAnimState].ubHeight == ANIM_CROUCH ) + { + pSoldier->EVENT_InitNewSoldierAnim(CUTTING_FENCE, 0, FALSE); + } + if ( pSoldier->bVisible != -1 ) + { + PlayJA2Sample(THROW_IMPACT_2, RATE_11025, SoundVolume(MIDVOLUME, pSoldier->sGridNo), 1, SoundDir(pSoldier->sGridNo)); + } + ActionDone(pSoldier); + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("CancelAIAction (AI_ACTION_PLANT_BOMB): failed to move object")); + CancelAIAction(pSoldier, FORCE); + EndAIGuysTurn(pSoldier); + } + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("CancelAIAction (AI_ACTION_PLANT_BOMB): failed to find bomb in hand")); + CancelAIAction(pSoldier, FORCE); + EndAIGuysTurn(pSoldier); + } + + break; + + default: #ifdef BETAVERSION NumMessage("ExecuteAction - Illegal action type = ",pSoldier->aiData.bAction); #endif diff --git a/TacticalAI/AIUtils.cpp b/TacticalAI/AIUtils.cpp index d1160f681..454c7a960 100644 --- a/TacticalAI/AIUtils.cpp +++ b/TacticalAI/AIUtils.cpp @@ -27,6 +27,7 @@ #include "SmokeEffects.h" // sevenfm #include "GameInitOptionsScreen.h" +#include "Structure Wrap.h" ////////////////////////////////////////////////////////////////////////////// // SANDRO - In this file, all APBPConstants[AP_CROUCH] and APBPConstants[AP_PRONE] were changed to GetAPsCrouch() and GetAPsProne() @@ -812,6 +813,18 @@ BOOLEAN IsActionAffordable(SOLDIERTYPE *pSoldier, INT8 bAction) bMinPointsNeeded = 20; // TODO break; + case AI_ACTION_DRINK_CANTEEN: + bMinPointsNeeded = APBPConstants[AP_DRINK]; + break; + + case AI_ACTION_HANDLE_ITEM: + bMinPointsNeeded = 0; + break; + + case AI_ACTION_PLANT_BOMB: + bMinPointsNeeded = APBPConstants[AP_INVENTORY_ARM] + APBPConstants[AP_DROP_BOMB]; + break; + default: #ifdef BETAVERSION //NumMessage("AffordableAction - Illegal action type = ",pSoldier->aiData.bAction); @@ -6327,3 +6340,221 @@ BOOLEAN CheckSuppressionDirection(SOLDIERTYPE *pSoldier, INT32 sTargetGridNo, IN return TRUE; } + +UINT8 CountKnownEnemies(SOLDIERTYPE* pSoldier, INT32 sSpot, INT16 sDistance, INT8 bLevel) +{ + CHECKF(pSoldier); + + SOLDIERTYPE* pOpponent; + INT32 sThreatLoc; + INT8 bThreatLevel; + UINT8 ubNum = 0; + + // loop through all the enemies + for ( UINT32 uiLoop = 0; uiLoop < guiNumMercSlots; ++uiLoop ) + { + pOpponent = MercSlots[uiLoop]; + + // if this merc is inactive, at base, on assignment, dead, unconscious + if ( !pOpponent || pOpponent->stats.bLife < OKLIFE ) + { + continue; + } + + if ( !ValidOpponent(pSoldier, pOpponent) ) + { + continue; + } + + // check knowledge + if ( Knowledge(pSoldier, pOpponent->ubID) == NOT_HEARD_OR_SEEN ) + { + continue; + } + + sThreatLoc = KnownLocation(pSoldier, pOpponent->ubID); + bThreatLevel = KnownLevel(pSoldier, pOpponent->ubID); + + if ( TileIsOutOfBounds(sThreatLoc) ) + { + continue; + } + + if ( PythSpacesAway(sSpot, sThreatLoc) > sDistance ) + { + continue; + } + + if ( bLevel >= 0 && bThreatLevel != bLevel ) + { + continue; + } + + ubNum++; + } + + return ubNum; +} + +UINT8 CountKnownEnemiesInRoom(SOLDIERTYPE* pSoldier, UINT16 usRoom) +{ + CHECKF(pSoldier); + + UINT8 ubNum = 0; + for ( UINT32 uiLoop = 0; uiLoop < guiNumMercSlots; ++uiLoop ) + { + SOLDIERTYPE* pOpponent = MercSlots[uiLoop]; + + // if this merc is inactive, at base, on assignment, dead, unconscious + if ( !pOpponent || pOpponent->stats.bLife < OKLIFE ) + { + continue; + } + + if ( !ValidOpponent(pSoldier, pOpponent) ) + { + continue; + } + + // check public knowledge + if ( Knowledge(pSoldier, pOpponent->ubID) == NOT_HEARD_OR_SEEN ) + { + continue; + } + + INT32 sThreatLoc = KnownLocation(pSoldier, pOpponent->ubID); + + if ( TileIsOutOfBounds(sThreatLoc) ) + { + continue; + } + + // check room + UINT16 usRoomNo; + if ( !InARoom(sThreatLoc, &usRoomNo) ) + { + continue; + } + + if ( usRoomNo != usRoom ) + { + continue; + } + + ubNum++; + } + + return ubNum; +} + +UINT8 CountFriendsInRoom(SOLDIERTYPE* pSoldier, UINT16 usRoom) +{ + CHECKF(pSoldier); + + SOLDIERTYPE* pFriend; + UINT8 ubFriendCount = 0; + UINT16 usRoomNo; + + // Run through each friendly. + for ( SoldierID iCounter = gTacticalStatus.Team[pSoldier->bTeam].bFirstID; iCounter <= gTacticalStatus.Team[pSoldier->bTeam].bLastID; ++iCounter ) + { + pFriend = iCounter; + + if ( pFriend && + pFriend != pSoldier && + pFriend->bActive && + pFriend->stats.bLife >= OKLIFE && + InARoom(pFriend->sGridNo, &usRoomNo) && + usRoomNo == usRoom ) + { + ubFriendCount++; + } + } + + return ubFriendCount; +} + +INT32 CountCorpsesInRoom(SOLDIERTYPE* pSoldier, UINT16 usRoomNo, INT8 bLevel) +{ + CHECKF(pSoldier); + + ROTTING_CORPSE* pCorpse; + INT32 iCount = 0; + + for ( INT32 cnt = 0; cnt < giNumRottingCorpse; ++cnt ) + { + pCorpse = &(gRottingCorpse[cnt]); + + if ( pCorpse && + pCorpse->fActivated && + pCorpse->def.ubType < ROTTING_STAGE2 && + pCorpse->def.ubBodyType <= REGFEMALE && + pCorpse->def.ubAIWarningValue > 0 && + pCorpse->def.bLevel == bLevel && + !TileIsOutOfBounds(pCorpse->def.sGridNo) && + RoomNo(pCorpse->def.sGridNo) == usRoomNo && + (pSoldier->bTeam == ENEMY_TEAM && CorpseEnemyTeam(pCorpse) || pSoldier->bTeam == MILITIA_TEAM && CorpseMilitiaTeam(pCorpse) || pSoldier->bTeam == CIV_TEAM && !pSoldier->aiData.bNeutral) ) + { + iCount++; + } + } + + return iCount; +} + +BOOLEAN FindFenceAroundSpot(INT32 sSpot) +{ + if ( TileIsOutOfBounds(sSpot) ) + { + return FALSE; + } + + INT32 sTempSpot; + + // check adjacent locations + for ( UINT8 ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++ ) + { + sTempSpot = NewGridNo(sSpot, DirectionInc(ubDirection)); + + if ( sTempSpot != sSpot && IsCuttableWireFenceAtGridNo(sTempSpot) ) + { + return TRUE; + } + } + + return FALSE; +} + +BOOLEAN SameRoom(INT32 sSpot1, INT32 sSpot2) +{ + if ( RoomNo(sSpot1) == RoomNo(sSpot2) && sSpot1 != NO_ROOM ) + return TRUE; + + return FALSE; +} + +UINT16 RoomNo(INT32 sSpot) +{ + if ( TileIsOutOfBounds(sSpot) ) + return NO_ROOM; + + return gusWorldRoomInfo[sSpot]; +} + +BOOLEAN CheckWindow(INT32 sSpot, UINT8 ubDirection, BOOLEAN fAllowClosed) +{ + CHECKF(!TileIsOutOfBounds(sSpot)); + + // find window spot + INT32 sWindowSpot = sSpot; + if ( ubDirection == NORTH || ubDirection == WEST ) + sWindowSpot = NewGridNo(sSpot, (UINT16)DirectionInc(ubDirection)); + + //if (IsJumpableWindowPresentAtGridNo(sWindowSpot, ubDirection, gGameExternalOptions.fCanJumpThroughClosedWindows)) + if ( IsJumpableWindowPresentAtGridNo(sWindowSpot, ubDirection, fAllowClosed) ) + { + return TRUE; + } + + return FALSE; +} diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index d6bdacbe6..fcc6ec4e2 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -11065,7 +11065,7 @@ static ActionType DecideActionVIPretreat(SOLDIERTYPE* pSoldier, bool logAction) //////////////////////////////////////////////////////////////////////////// // CHANGE STANCE //////////////////////////////////////////////////////////////////////////// -ActionType DecideActionChangeStance(SOLDIERTYPE* pSoldier, UINT8 ubCanMove, ATTACKTYPE BestAttack, UINT8 ubBestAttackAction, bool logAction) +static ActionType DecideActionChangeStance(SOLDIERTYPE* pSoldier, UINT8 ubCanMove, ATTACKTYPE BestAttack, UINT8 ubBestAttackAction, bool logAction) { // if not in water and not already crouched, try to crouch down first if (!gfTurnBasedAI || GetAPsToChangeStance(pSoldier, ANIM_CROUCH) <= pSoldier->bActionPoints) @@ -11170,6 +11170,180 @@ static ActionType MoveCloserBeforeShooting(SOLDIERTYPE* pSoldier, ATTACKTYPE att return(AI_ACTION_INVALID); } + +static ActionType DecideBlowUpObstacle(SOLDIERTYPE* pSoldier, INT32 sClosestOpponent) +{ + INT8 bTNTSlot = FindTNT(pSoldier); + + if ( bTNTSlot != NO_SLOT && + pSoldier->bTeam == ENEMY_TEAM && + IsActionAffordable(pSoldier, AI_ACTION_PLANT_BOMB) && + pSoldier->pathing.bLevel == 0 && + !TileIsOutOfBounds(sClosestOpponent) && + (Chance(SoldierDifficultyLevel(pSoldier) * 10) || + InARoom(sClosestOpponent, NULL) && + CountFriendsInRoom(pSoldier, RoomNo(sClosestOpponent)) == 0 && + !SameRoom(sClosestOpponent, pSoldier->sGridNo) && + (CountCorpsesInRoom(pSoldier, RoomNo(sClosestOpponent), 0) > 0 || TeamHighPercentKilled(pSoldier->bTeam) || EstimatePathCostToLocation(pSoldier, sClosestOpponent, pSoldier->pathing.bLevel, FALSE, NULL, NULL) == 0)) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("found TNT in slot %d, check to plant bomb", bTNTSlot)); + + const INT8 bLevel = pSoldier->pathing.bLevel; + UINT8 ubDesiredDir = AIDirection(pSoldier->sGridNo, sClosestOpponent); + INT32 sCheckSpot; + INT32 sPathCost, sNewPathCost; + INT32 sOriginalGridNo; + UINT16 usRoom; + + for ( UINT8 ubCheckDir = 0; ubCheckDir < NUM_WORLD_DIRECTIONS; ubCheckDir++ ) + { + INT32 sNewSpot = NewGridNo(pSoldier->sGridNo, DirectionInc(ubCheckDir)); + INT32 sNextSpot = NewGridNo(sNewSpot, DirectionInc(ubCheckDir)); + + if ( sNewSpot == pSoldier->sGridNo || + sNextSpot == sNewSpot || + gpWorldLevelData[sNewSpot].sHeight != gpWorldLevelData[pSoldier->sGridNo].sHeight || + gpWorldLevelData[sNextSpot].sHeight != gpWorldLevelData[pSoldier->sGridNo].sHeight || + //ubCheckDir != ubDesiredDir && ubCheckDir != gOneCDirection[ubDesiredDir] && ubCheckDir != gOneCCDirection[ubDesiredDir] || + gubWorldMovementCosts[sNewSpot][ubCheckDir][bLevel] < TRAVELCOST_BLOCKED ) + { + continue; + } + + sCheckSpot = NOWHERE; + + if ( (IsCuttableWireFenceAtGridNo(sNewSpot) && !IsCutWireFenceAtGridNo(sNewSpot) || gubWorldMovementCosts[sNewSpot][ubCheckDir][bLevel] == TRAVELCOST_OBSTACLE) && + IsLocationSittable(sNextSpot, bLevel) && NewOKDestination(pSoldier, sNextSpot, TRUE, bLevel) && !Water(sNextSpot, bLevel) ) + { + // found fence, jump 2 tiles + sCheckSpot = sNextSpot; + } + else if ( (gubWorldMovementCosts[sNewSpot][ubCheckDir][bLevel] == TRAVELCOST_WALL || gubWorldMovementCosts[sNewSpot][ubCheckDir][bLevel] == TRAVELCOST_DOOR) && + IsLocationSittable(sNewSpot, bLevel) && NewOKDestination(pSoldier, sNewSpot, TRUE, bLevel) && !Water(sNewSpot, bLevel) ) + { + // found wall, jump 1 tile + sCheckSpot = sNewSpot; + } + + if ( !TileIsOutOfBounds(sCheckSpot) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("check if jumping to %d improves path cost", sCheckSpot)); + + // check if removing obstacle improves situation + sPathCost = EstimatePlotPath(pSoldier, sClosestOpponent, FALSE, FALSE, FALSE, RUNNING, pSoldier->bStealthMode, FALSE, 0); + sOriginalGridNo = pSoldier->sGridNo; + pSoldier->sGridNo = sCheckSpot; + sNewPathCost = EstimatePlotPath(pSoldier, sClosestOpponent, FALSE, FALSE, FALSE, RUNNING, pSoldier->bStealthMode, FALSE, 0); + pSoldier->sGridNo = sOriginalGridNo; + + if ( sNewPathCost > 0 && + (sPathCost == 0 || + sPathCost > sNewPathCost && sPathCost - sNewPathCost > APBPConstants[AP_MAXIMUM] || + InARoom(sCheckSpot, &usRoom) && + !SameRoom(sCheckSpot, pSoldier->sGridNo) && + CountFriendsInRoom(pSoldier, usRoom) == 0 && + CountKnownEnemiesInRoom(pSoldier, usRoom) > 0 && + (CountCorpsesInRoom(pSoldier, usRoom, 0) > 0 || TeamHighPercentKilled(pSoldier->bTeam))) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("blowing up fence/wall/obstacle improves path cost, plant bomb")); + + RearrangePocket(pSoldier, HANDPOS, bTNTSlot, FOREVER); + pSoldier->aiData.usActionData = 0; + + return AI_ACTION_PLANT_BOMB; + } + } + } + } + + return AI_ACTION_INVALID; +} + + +static ActionType DecideJumpWindow(SOLDIERTYPE* pSoldier, INT32 sClosestOpponent) +{ + if ( gGameExternalOptions.fCanJumpThroughWindows && + !pSoldier->bBlindedCounter && + !TileIsOutOfBounds(sClosestOpponent) && + pSoldier->pathing.bLevel == 0 && + pSoldier->AnimEndHeight() >= ANIM_CROUCH && + IsActionAffordable(pSoldier, AI_ACTION_JUMP_WINDOW) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("check if we can jump through window")); + + const INT8 bLevel = pSoldier->pathing.bLevel; + + for ( UINT8 ubCheckDir = 0; ubCheckDir < NUM_WORLD_DIRECTIONS; ubCheckDir++ ) + { + // cannot jump diagonally + if ( ubCheckDir % 2 != 0 ) + { + continue; + } + + INT32 sNewSpot = NewGridNo(pSoldier->sGridNo, DirectionInc(ubCheckDir)); + + if ( sNewSpot != pSoldier->sGridNo && + CheckWindow(pSoldier->sGridNo, ubCheckDir, gGameExternalOptions.fCanJumpThroughClosedWindows) && + !Water(sNewSpot, bLevel) && + IsLocationSittable(sNewSpot, bLevel) && + NewOKDestination(pSoldier, sNewSpot, TRUE, bLevel) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("found jumpable window at %d", sNewSpot)); + + // check if jumping improves situation + INT32 sPathCost = EstimatePlotPath(pSoldier, sClosestOpponent, FALSE, FALSE, FALSE, RUNNING, pSoldier->bStealthMode, FALSE, 0); + INT32 sOriginalGridNo = pSoldier->sGridNo; + pSoldier->sGridNo = sNewSpot; + INT32 sNewPathCost = EstimatePlotPath(pSoldier, sClosestOpponent, FALSE, FALSE, FALSE, RUNNING, pSoldier->bStealthMode, FALSE, 0); + pSoldier->sGridNo = sOriginalGridNo; + + if ( sNewPathCost > 0 && (sPathCost == 0 || sPathCost > sNewPathCost && sPathCost - sNewPathCost > APBPConstants[AP_MAXIMUM] || SameRoom(sClosestOpponent, sNewSpot)) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("jumping improves path cost")); + + if ( gfTurnBasedAI && pSoldier->bActionPoints < pSoldier->bInitialActionPoints ) + { + if ( pSoldier->ubDirection != ubCheckDir && + pSoldier->InternalIsValidStance(ubCheckDir, gAnimControl[pSoldier->usAnimState].ubEndHeight) && + pSoldier->bActionPoints >= GetAPsToLook(pSoldier) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("turn to window, end turn before jumping")); + pSoldier->aiData.usActionData = ubCheckDir; + pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + pSoldier->aiData.usNextActionData = 0; + return AI_ACTION_CHANGE_FACING; + } + else + { + DebugAI(AI_MSG_INFO, pSoldier, String("already facing window, end turn before jumping")); + pSoldier->aiData.usActionData = 0; + return AI_ACTION_END_TURN; + } + } + + if ( pSoldier->ubDirection == ubCheckDir ) + { + pSoldier->aiData.usActionData = 0; + return AI_ACTION_JUMP_WINDOW; + } + else if ( pSoldier->InternalIsValidStance(ubCheckDir, gAnimControl[pSoldier->usAnimState].ubEndHeight) && + pSoldier->bActionPoints >= GetAPsToJumpThroughWindows(pSoldier, FALSE) + GetAPsToLook(pSoldier) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("turn before jumping")); + pSoldier->aiData.usActionData = ubCheckDir; + pSoldier->aiData.bNextAction = AI_ACTION_JUMP_WINDOW; + pSoldier->aiData.usNextActionData = 0; + return AI_ACTION_CHANGE_FACING; + } + } + } + } + } + + return AI_ACTION_INVALID; +} + //----------------------------------------------------------------- // Boxer AI decision routines //----------------------------------------------------------------- @@ -20533,6 +20707,7 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) INT8 bOpponentLevel; INT32 distanceToOpponent; INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, &sOpponentGridNo, &bOpponentLevel, NULL, &distanceToOpponent); + INT32 sClosestSeenOpponent = ClosestSeenOpponent(pSoldier, NULL, NULL); DebugAI(AI_MSG_INFO, pSoldier, String("sClosestOpponent %d", sClosestOpponent)); @@ -20623,6 +20798,25 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) pSoldier->aiData.bAIMorale = CalcMorale(pSoldier); + //////////////////////////////////////////////////////////////////////// + // DRINK IF LOW IN BREATH + //////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_TOPIC, pSoldier, String("[drink if low on breath]")); + + if ( + !bInWater && + !fDangerousSpot && + pSoldier->bBreath < OKBREATH && + pSoldier->CheckInitialAP() && + Chance(30 - 10 * pSoldier->aiData.bUnderFire) && + IsActionAffordable(pSoldier, AI_ACTION_DRINK_CANTEEN) && + FindCanteen(pSoldier) != NO_SLOT ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("drink from canteen")); + return AI_ACTION_DRINK_CANTEEN; + } + + //////////////////////////////////////////////////////////////////////////// // IF GASSED, OR REALLY TIRED (ON THE VERGE OF COLLAPSING), TRY TO RUN AWAY //////////////////////////////////////////////////////////////////////////// @@ -20826,6 +21020,148 @@ INT8 DecideActionBlackSoldier(SOLDIERTYPE* pSoldier) } } + + //////////////////////////////////////////////////////////////////////////// + // USE EXPLOSIVES + //////////////////////////////////////////////////////////////////////////// + INT8 bTNTSlot = FindTNT(pSoldier); + if ( bTNTSlot != NO_SLOT && + (pSoldier->CheckInitialAP() || gfTurnBasedAI) && + IsActionAffordable(pSoldier, AI_ACTION_PLANT_BOMB) && + !pSoldier->aiData.bUnderFire && + //(pSoldier->aiData.bOrders == SEEKENEMY || pSoldier->RushAttackActive()) && + pSoldier->aiData.bOrders == SEEKENEMY && + !TileIsOutOfBounds(sClosestSeenOpponent) && + PythSpacesAway(pSoldier->sGridNo, sClosestSeenOpponent) > TACTICAL_RANGE_HALF && + CountKnownEnemies(pSoldier, pSoldier->sGridNo, 2) > 0 && + (Chance(SoldierDifficultyLevel(pSoldier) * 10) || InARoom(pSoldier->sGridNo, nullptr) && CountKnownEnemies(pSoldier, pSoldier->sGridNo, 2) > 0) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("found TNT in slot %d, plant bomb", bTNTSlot)); + + RearrangePocket(pSoldier, HANDPOS, bTNTSlot, FOREVER); + pSoldier->aiData.usActionData = 0; + return AI_ACTION_PLANT_BOMB; + } + + + if ( pSoldier->bTeam == ENEMY_TEAM && + !pSoldier->bBlindedCounter && + (pSoldier->CheckInitialAP() || gfTurnBasedAI) && + !pSoldier->aiData.bUnderFire && + pSoldier->aiData.bAIMorale >= MORALE_CONFIDENT && + //(pSoldier->aiData.bOrders == SEEKENEMY || pSoldier->RushAttackActive()) && + pSoldier->aiData.bOrders == SEEKENEMY && + !TileIsOutOfBounds(sClosestOpponent) && + (Chance(SoldierDifficultyLevel(pSoldier) * 10) || + InARoom(sClosestOpponent, nullptr) && + CountFriendsInRoom(pSoldier, RoomNo(sClosestOpponent)) == 0 && + !SameRoom(sClosestOpponent, pSoldier->sGridNo) && + (CountCorpsesInRoom(pSoldier, RoomNo(sClosestOpponent), 0) > 0 || TeamHighPercentKilled(pSoldier->bTeam) || EstimatePathCostToLocation(pSoldier, sClosestOpponent, pSoldier->pathing.bLevel, FALSE, NULL, NULL) == 0)) ) + { + decision = DecideBlowUpObstacle(pSoldier, sClosestOpponent); + if ( decision != AI_ACTION_INVALID ) + { + return decision; + } + } + + + //////////////////////////////////////////////////////////////////////////// + // CUT A FENCE WITH WIRECUTTERS + //////////////////////////////////////////////////////////////////////////// + INT8 bWirecutterSlot = FindWirecutters(pSoldier); + if ( + (pSoldier->CheckInitialAP() || gfTurnBasedAI) && + !pSoldier->aiData.bUnderFire && + bWirecutterSlot != NO_SLOT && + pSoldier->pathing.bLevel == 0 && + pSoldier->aiData.bAIMorale >= MORALE_CONFIDENT && + //(pSoldier->aiData.bOrders == SEEKENEMY || pSoldier->RushAttackActive()) && + pSoldier->aiData.bOrders == SEEKENEMY && + !TileIsOutOfBounds(sClosestOpponent) && + Chance(SoldierDifficultyLevel(pSoldier) * 20) && + pSoldier->bActionPoints >= GetAPsToCutFence(pSoldier) + GetAPsToLook(pSoldier) && + FindFenceAroundSpot(pSoldier->sGridNo) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("found wire cutter, check if we can find fence to cut")); + + UINT8 ubDesiredDir = AIDirection(pSoldier->sGridNo, sClosestOpponent); + INT32 sNewSpot; + INT32 sNextSpot; + INT32 sPathCost, sNewPathCost; + INT32 sOriginalGridNo; + + for ( UINT8 ubCheckDir = 0; ubCheckDir < NUM_WORLD_DIRECTIONS; ubCheckDir++ ) + { + // cannot cut fence diagonally, check desired dir + if ( ubCheckDir % 2 != 0 || + ubCheckDir != ubDesiredDir && + ubCheckDir != gOneCDirection[ubDesiredDir] && + ubCheckDir != gOneCCDirection[ubDesiredDir] ) + { + continue; + } + + sNewSpot = NewGridNo(pSoldier->sGridNo, DirectionInc(ubCheckDir)); + sNextSpot = NewGridNo(sNewSpot, DirectionInc(ubCheckDir)); + + if ( sNewSpot != pSoldier->sGridNo && + sNextSpot != sNewSpot && + !Water(sNewSpot, pSoldier->pathing.bLevel) && + !Water(sNextSpot, pSoldier->pathing.bLevel) && + IsCuttableWireFenceAtGridNo(sNewSpot) && + !IsCutWireFenceAtGridNo(sNewSpot) && + IsLocationSittable(sNextSpot, pSoldier->pathing.bLevel) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("found cuttable fence at %d", sNewSpot)); + + // check if cutting the fence improves situation + sPathCost = EstimatePlotPath(pSoldier, sClosestOpponent, FALSE, FALSE, FALSE, RUNNING, pSoldier->bStealthMode, FALSE, 0); + sOriginalGridNo = pSoldier->sGridNo; + pSoldier->sGridNo = sNewSpot; + sNewPathCost = EstimatePlotPath(pSoldier, sClosestOpponent, FALSE, FALSE, FALSE, RUNNING, pSoldier->bStealthMode, FALSE, 0); + pSoldier->sGridNo = sOriginalGridNo; + + if ( sNewPathCost > 0 && (sPathCost == 0 || sPathCost > sNewPathCost && sPathCost - sNewPathCost > APBPConstants[AP_MAXIMUM]) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("cutting fence improves path cost, use wire cutter")); + if ( pSoldier->ubDirection == ubCheckDir ) + { + RearrangePocket(pSoldier, HANDPOS, bWirecutterSlot, FOREVER); + pSoldier->aiData.usActionData = sNewSpot; + return AI_ACTION_HANDLE_ITEM; + } + else if ( pSoldier->InternalIsValidStance(ubCheckDir, gAnimControl[pSoldier->usAnimState].ubEndHeight) ) + { + DebugAI(AI_MSG_INFO, pSoldier, String("turn before cutting fence")); + RearrangePocket(pSoldier, HANDPOS, bWirecutterSlot, FOREVER); + pSoldier->aiData.usActionData = ubCheckDir; + pSoldier->aiData.bNextAction = AI_ACTION_HANDLE_ITEM; + pSoldier->aiData.usNextActionData = sNewSpot; + return AI_ACTION_CHANGE_FACING; + } + } + } + } + } + + + //////////////////////////////////////////////////////////////////////////// + // JUMP THROUGH WINDOW + //////////////////////////////////////////////////////////////////////////// + DebugAI(AI_MSG_TOPIC, pSoldier, String("[try to jump into window]")); + if ( (pSoldier->CheckInitialAP() || gfTurnBasedAI) && + !pSoldier->aiData.bUnderFire && + pSoldier->aiData.bAIMorale >= MORALE_CONFIDENT && + pSoldier->aiData.bOrders == SEEKENEMY && + !TileIsOutOfBounds(sClosestOpponent) ) + { + decision = DecideJumpWindow(pSoldier, sClosestOpponent); + if ( decision != AI_ACTION_INVALID ) + return decision; + } + + //////////////////////////////////////////////////////////////////////////// // THROW A SMOKE GRENADE FOR COVER //////////////////////////////////////////////////////////////////////////// diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index e6f16d455..af5aa1c9b 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -106,6 +106,9 @@ typedef enum AI_ACTION_DOCTOR_SELF, // added by Flugente: AI-ONLY! bandage/surgery on self. DO NOT USE THIS FOR MERCS!!! AI_ACTION_SELFDETONATE, // added by Flugente: blow up an explosive in own inventory AI_ACTION_STOP_MEDIC, // sevenfm: stop giving aid animation + AI_ACTION_DRINK_CANTEEN, // sevenfm: drink from canteen in inventory + AI_ACTION_HANDLE_ITEM, // sevenfm: use item in hand + AI_ACTION_PLANT_BOMB, // sevenfm: plant bomb using item in hand AI_ACTION_INVALID } ActionType; @@ -215,6 +218,7 @@ INT32 FindClosestDoor( SOLDIERTYPE * pSoldier ); INT32 FindNearbyPointOnEdgeOfMap( SOLDIERTYPE * pSoldier, INT8 * pbDirection ); INT32 FindNearestEdgePoint( INT32 sGridNo ); INT32 FindNearestPassableSpot( INT32 sGridNo, UINT8 usSearchRadius = 5 ); +BOOLEAN FindFenceAroundSpot(INT32 sSpot); //Kris: Added these as I need specific searches on certain sides. enum @@ -312,6 +316,13 @@ UINT8 CountFriendsBlack( SOLDIERTYPE *pSoldier, INT32 sClosestOpponent = NOWHERE UINT16 CountTeamUnderAttack(INT8 bTeam, INT32 sGridNo, INT16 sDistance); UINT16 CountPublicKnownEnemies(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT16 sDistance); UINT16 CountPublicKnownEnemies(SOLDIERTYPE *pSoldier); +UINT8 CountKnownEnemies(SOLDIERTYPE* pSoldier, INT32 sSpot, INT16 sDistance, INT8 bLevel = HEARD_THIS_TURN); +UINT8 CountKnownEnemiesInRoom(SOLDIERTYPE* pSoldier, UINT16 usRoom); +UINT8 CountFriendsInRoom(SOLDIERTYPE* pSoldier, UINT16 usRoom); +INT32 CountCorpsesInRoom(SOLDIERTYPE* pSoldier, UINT16 usRoomNo, INT8 bLevel); +UINT16 RoomNo(INT32 sSpot); +BOOLEAN SameRoom(INT32 sSpot1, INT32 sSpot2); +BOOLEAN CheckWindow(INT32 sSpot, UINT8 ubDirection, BOOLEAN fAllowClosed); UINT8 SectorCurfew(BOOLEAN fNight); UINT8 TeamPercentKilled(INT8 bTeam); @@ -434,6 +445,7 @@ INT8 KnownPublicLevel(UINT8 bTeam, SoldierID ubOpponentID); #define TACTICAL_RANGE (gGameExternalOptions.ubStraightSightRange * STRAIGHT_RATIO * 2) #define TACTICAL_RANGE_CELL_COORDS TACTICAL_RANGE*CELL_X_SIZE #define BOMB_DETECTION_RANGE (TACTICAL_RANGE / 4) +#define TACTICAL_RANGE_HALF (TACTICAL_RANGE / 2) #define TACTICAL_RANGE_CLOSE (TACTICAL_RANGE / 4) #define TACTICAL_RANGE_VERYCLOSE (TACTICAL_RANGE / 6) From caf925b2160812ff93515515e0dd706044a1f819 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:35:03 +0200 Subject: [PATCH 79/80] Fix Fatima not moving after map traversal --- ModularizedTacticalAI/src/CivilianPlan.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ModularizedTacticalAI/src/CivilianPlan.cpp b/ModularizedTacticalAI/src/CivilianPlan.cpp index abd302bcc..7a20ca95d 100644 --- a/ModularizedTacticalAI/src/CivilianPlan.cpp +++ b/ModularizedTacticalAI/src/CivilianPlan.cpp @@ -1,5 +1,7 @@ #include "../include/CivilianPlan.h" #include "../../TacticalAI/ai.h" +#include "NPC.h" +#include "Soldier Profile.h" namespace AI { @@ -13,6 +15,20 @@ namespace AI void CivilianPlan::execute(PlanInputData& environment) { + if ( !environment.turn_based() ) + { + if ( (get_npc()->ubProfile != NO_PROFILE) && (gMercProfiles[get_npc()->ubProfile].ubMiscFlags3 & PROFILE_MISC_FLAG3_HANDLE_DONE_TRAVERSAL) ) + { + TriggerNPCWithGivenApproach(get_npc()->ubProfile, APPROACH_DONE_TRAVERSAL, FALSE); + gMercProfiles[get_npc()->ubProfile].ubMiscFlags3 &= (~PROFILE_MISC_FLAG3_HANDLE_DONE_TRAVERSAL); + get_npc()->ubQuoteActionID = 0; + // wait a tiny bit + get_npc()->aiData.usActionData = 100; + get_npc()->aiData.bAction = AI_ACTION_WAIT; + return; + } + } + switch (get_npc()->aiData.bAlertStatus) { case STATUS_GREEN: From 7b8c2d1ff90d92c762bb6720afe20e6ea83dfe69 Mon Sep 17 00:00:00 2001 From: Asdow <20314541+Asdow@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:35:59 +0200 Subject: [PATCH 80/80] Remove old function call --- TacticalAI/AIMain.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index d4935daad..0bb9c1439 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -2744,7 +2744,6 @@ INT8 ExecuteAction(SOLDIERTYPE *pSoldier) case AI_ACTION_JUMP_WINDOW: { - pSoldier->BeginSoldierClimbWindow(); pSoldier->BeginSoldierJumpWindowAI(); if ( gfTurnBasedAI ) {