From 93c41f2f974089583346dcbebf8620784dbc73e9 Mon Sep 17 00:00:00 2001 From: Sevenfm Date: Thu, 6 Aug 2020 07:53:12 +0000 Subject: [PATCH] AI suppression fire improvements. CalcBestShot: - added standard check for valid opponent - use standard knowledge functions - blind soldier can only attack seen/heard personally - determine suppression fire (shooting at target nobody sees currently) - don't shoot if target location is seen and empty - no fire on unseen opponents with throwing knives - only enemy team can use blind suppression fire - only try to suppress alive and conscious human targets - additional check to limit shooting through walls - alt weapon holding scope mode is used only when ubAllowAlternativeWeaponHolding == 3 - hip firing allowed only for human bodytypes - take into account direction when checking stance - shoot heavy guns in standing stance only when using hip fire - penalize suppression fire Red AI suppression fire: - check valid target - check weapon/ammo requirements - check that soldier is not sniper - check friendly fire chance - reduce chance to shoot if target is beyond weapon range - check that we have spare ammo Improved code to prepare suppression fire: - change stance first of needed - allow shooting with aiming - reserve APs to hide if no cover or enemy is close - added debug messages git-svn-id: https://ja2svn.mooo.com/source/ja2/trunk/GameSource/ja2_v1.13/Build@8878 3b4a5df2-a311-0410-b5c6-a8a6f20db521 --- TacticalAI/AIUtils.cpp | 78 ++++++++++ TacticalAI/Attacks.cpp | 275 ++++++++++++++++++++++++++---------- TacticalAI/DecideAction.cpp | 138 ++++++++++++------ TacticalAI/ai.h | 5 +- 4 files changed, 379 insertions(+), 117 deletions(-) diff --git a/TacticalAI/AIUtils.cpp b/TacticalAI/AIUtils.cpp index 50be8986..d4a70911 100644 --- a/TacticalAI/AIUtils.cpp +++ b/TacticalAI/AIUtils.cpp @@ -5790,3 +5790,81 @@ UINT8 SectorCurfew(BOOLEAN fNight) return ubSectorData; } + +INT32 RandomizeLocation(INT32 sSpot, INT8 bLevel, UINT8 ubTimes, SOLDIERTYPE *pSightSoldier) +{ + if (TileIsOutOfBounds(sSpot)) + { + return NOWHERE; + } + + UINT8 ubDirection; + UINT8 ubMovementCost; + INT32 sTempSpot; + INT32 sSpotArray[NUM_WORLD_DIRECTIONS + 1]; + UINT8 ubSpots; + + for (UINT8 ubCnt = 0; ubCnt < ubTimes; ubCnt++) + { + // store original location + ubSpots = 1; + sSpotArray[0] = sSpot; + + // find adjacent locations + for (ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++) + { + sTempSpot = NewGridNo(sSpot, DirectionInc(ubDirection)); + + if (sTempSpot != sSpot) + { + ubMovementCost = gubWorldMovementCosts[sTempSpot][ubDirection][bLevel]; + + if (ubMovementCost < TRAVELCOST_BLOCKED && + IsLocationSittableExcludingPeople(sTempSpot, bLevel) && + (!pSightSoldier || SoldierToVirtualSoldierLineOfSightTest(pSightSoldier, sTempSpot, bLevel, ANIM_STAND, TRUE, NO_DISTANCE_LIMIT))) + { + sSpotArray[ubSpots] = sTempSpot; + ubSpots++; + } + } + } + // find random location + sSpot = sSpotArray[Random(ubSpots)]; + // stop if could not find any adjacent spot + if (ubSpots < 2) + { + break; + } + } + + return sSpot; +} + +INT32 RandomizeOpponentLocation(INT32 sSpot, SOLDIERTYPE *pOpponent, INT16 sMaxDistance) +{ + if (TileIsOutOfBounds(sSpot)) + { + return NOWHERE; + } + + INT8 bXOffset, bYOffset; + INT32 sRandomSpot; + + if (sMaxDistance > 0) + { + for (INT cnt = 0; cnt < min(sMaxDistance * 2, 100); cnt++) + { + bXOffset = Random(sMaxDistance * 2 + 1) - sMaxDistance; + bYOffset = Random(sMaxDistance * 2 + 1) - sMaxDistance; + + sRandomSpot = sSpot + bXOffset + (MAXCOL * bYOffset); + + if (!TileIsOutOfBounds(sRandomSpot) && NewOKDestination(pOpponent, sRandomSpot, FALSE, pOpponent->pathing.bLevel)) + { + return sRandomSpot; + } + } + } + + return sSpot; +} \ No newline at end of file diff --git a/TacticalAI/Attacks.cpp b/TacticalAI/Attacks.cpp index dae1af53..a60980a6 100644 --- a/TacticalAI/Attacks.cpp +++ b/TacticalAI/Attacks.cpp @@ -32,6 +32,7 @@ #include "Isometric Utils.h" #include "Structure Wrap.h" // IsRoofPresentAtGridNo #include "Render Fun.h" +#include "worldman.h" #endif // anv: for enemy taunts @@ -170,15 +171,34 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) UINT32 uiLoop; INT32 iAttackValue, iThreatValue, iHitRate, iBestHitRate, iPercentBetter, iEstDamage, iTrueLastTarget; UINT16 usTrueState, usTurningCost, usRaiseGunCost; - INT16 ubAimTime, ubMinAPcost, ubRawAPCost, sBestAPcost, ubChanceToHit, ubBestAimTime, ubChanceToGetThrough, ubBestChanceToGetThrough, ubFriendlyFireChance, ubBestFriendlyFireChance, ubBestChanceToHit, sStanceAPcost; + INT16 ubAimTime; + INT16 ubMinAPcost; + INT16 ubRawAPCost; + INT16 sBestAPcost; + INT16 ubChanceToHit; + INT16 ubBestAimTime; + INT16 ubChanceToGetThrough; + INT16 ubBestChanceToGetThrough; + UINT8 ubFriendlyFireChance; + UINT8 ubBestFriendlyFireChance; + INT16 ubBestChanceToHit; + INT16 sStanceAPcost; BOOLEAN fAddingTurningCost, fAddingRaiseGunCost; UINT8 ubMaxPossibleAimTime, ubStance, ubBestStance, ubChanceToReallyHit; INT8 bScopeMode; SOLDIERTYPE *pOpponent; - DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"CalcBestShot"); + // sevenfm: + BOOLEAN fSuppression = FALSE; + BOOLEAN fReturnFire = FALSE; + INT32 sTarget = NOWHERE; + INT8 bLevel; - ubBestChanceToHit = ubBestAimTime = ubChanceToHit = ubBestChanceToGetThrough = ubBestFriendlyFireChance = ubChanceToReallyHit = 0; + INT8 bKnowledge; + INT8 bPersonalKnowledge; + INT8 bPublicKnowledge; + + DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"CalcBestShot"); // sevenfm: initialize pBestShot->ubPossible = FALSE; @@ -187,6 +207,8 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) pBestShot->ubOpponent = NOBODY; pBestShot->ubFriendlyFireChance = 0; + ubBestChanceToHit = ubBestAimTime = ubChanceToHit = ubBestChanceToGetThrough = ubBestFriendlyFireChance = ubChanceToReallyHit = 0; + // sevenfm: set attacking hand and target pSoldier->ubAttackingHand = HANDPOS; pSoldier->ubTargetID = NOBODY; @@ -204,40 +226,131 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { 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 this man is neutral / on the same side, he's not an opponent - if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide)) - continue; // next merc + if (!ValidOpponent(pSoldier, pOpponent)) + { + continue; + } - // if this opponent is not currently in sight (ignore known but unseen!) - /*if ((pSoldier->aiData.bOppList[pOpponent->ubID] != SEEN_CURRENTLY && !shootUnseen)) //guys we can't see + // determine return fire + if (pSoldier->aiData.bUnderFire && + !pSoldier->bBlindedCounter && + pSoldier->ubPreviousAttackerID == pOpponent->ubID) { - DebugMsg (TOPIC_JA2,DBG_LEVEL_3,String("CalcBestShot: soldier = %d, target = %d, skip guys we can't see, shootUnseen = %d, personal opplist = %d",pSoldier->ubID, pOpponent->ubID, shootUnseen, pSoldier->aiData.bOppList[pOpponent->ubID])); - continue; // next opponent - }*/ - if ((pSoldier->aiData.bOppList[pOpponent->ubID] != SEEN_CURRENTLY && gbPublicOpplist[pSoldier->bTeam][pOpponent->ubID] != SEEN_CURRENTLY)) // guys nobody sees + 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)) { - DebugMsg(TOPIC_JA2, DBG_LEVEL_3, String("CalcBestShot: soldier = %d, target = %d, skip guys nobody sees, public opplist = %d", pSoldier->ubID, pOpponent->ubID, gbPublicOpplist[pSoldier->bTeam][pOpponent->ubID])); continue; // next opponent } - // silversurfer: ignore empty vehicles - // sevenfm: allow shooting at empty vehicles, but they have low priority - //if ( pOpponent->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle( pOpponent->bVehicleID ) == 0 ) - //continue; - - // Special stuff for Carmen the bounty hunter - if (pSoldier->aiData.bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != SLAY) + // 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)) + { + fSuppression = TRUE; + } + + // 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)) + { + 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->usAnimState == COWERING || + //pOpponent->usAnimState == COWERING_PRONE || + CoweringShockLevel(pOpponent) || + pOpponent->IsZombie() || + !IS_MERC_BODY_TYPE(pOpponent))) + { + continue; + } + + // shoot through wall check + if( bPersonalKnowledge != SEEN_CURRENTLY && + !SoldierToSoldierLineOfSightTest(pSoldier, pOpponent, TRUE, 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 - ubMinAPcost = MinAPsToAttack(pSoldier, pOpponent->sGridNo, DONTADDTURNCOST, 0);// later will be decide if shoot is possible this here is just best guess so ignore turnover + ubMinAPcost = 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 (ubMinAPcost > pSoldier->bActionPoints) @@ -274,7 +387,7 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) INT8 bDir; // must make sure that structure data can be added in the direction of the target - bDir = (INT8) GetDirectionToGridNoFromGridNo( pSoldier->sGridNo, pOpponent->sGridNo ); + bDir = (INT8)GetDirectionToGridNoFromGridNo(pSoldier->sGridNo, sTarget); // ATE: Only if we have a levelnode... if ( pSoldier->pLevelNode != NULL && pSoldier->pLevelNode->pStructureData != NULL ) @@ -294,38 +407,37 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) } } - //if (pOpponent->sGridNo == pSoldier->sLastTarget) - //{ - // // raw AP cost calculation included cost of changing target! - // // Not unless we really needed to change targets! - // //ubRawAPCost -= APBPConstants[AP_CHANGE_TARGET]; - //} - iBestHitRate = 0; // reset best hit rate to minimum //dnl ch69 130913 Hoping to optimize // consider alternate holding mode and different scopes - for(pSoldier->bScopeMode=(gGameExternalOptions.ubAllowAlternativeWeaponHolding?USE_ALT_WEAPON_HOLD:USE_BEST_SCOPE); pSoldier->bScopeMode<=(gGameExternalOptions.fScopeModes?NUM_SCOPE_MODES-1:USE_BEST_SCOPE); pSoldier->bScopeMode++) + // 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++) { - //dnl ch71 180913 throwing knives cannot be use in fire from hip, also SANDRO said: if the gun is flagged as HeavyGun, then we can only fire it from hip, thus no scopes to use at all, not even iron sights 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; - } - else - { - if(Weapon[pSoldier->usAttackingWeapon].HeavyGun) + + // 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 + usTrueState = pSoldier->usAnimState; // because is used in CalculateRaiseGunCost, CalcAimingLevelsAvailableWithAP, CalculateTurningCost + iTrueLastTarget = pSoldier->sLastTarget; // because is used in MinAPsToShootOrStab // --------- Standing --------- ubStance = ANIM_STAND; - if(IsValidStance(pSoldier, ubStance)) + // 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 || !Item[pSoldier->usAttackingWeapon].twohanded || !gGameExternalOptions.ubAllowAlternativeWeaponHolding)) { sStanceAPcost = GetAPsToChangeStance(pSoldier, ubStance); if(sStanceAPcost) @@ -334,9 +446,9 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) pSoldier->usAnimState = STANDING; pSoldier->sLastTarget = NOWHERE; } - GetAPChargeForShootOrStabWRTGunRaises(pSoldier, pOpponent->sGridNo, TRUE, &fAddingTurningCost, &fAddingRaiseGunCost, 0); + GetAPChargeForShootOrStabWRTGunRaises(pSoldier, sTarget, TRUE, &fAddingTurningCost, &fAddingRaiseGunCost, 0); usTurningCost = CalculateTurningCost(pSoldier, pSoldier->usAttackingWeapon, fAddingTurningCost); - usRaiseGunCost = CalculateRaiseGunCost(pSoldier, fAddingRaiseGunCost, pOpponent->sGridNo, 0); + usRaiseGunCost = CalculateRaiseGunCost(pSoldier, fAddingRaiseGunCost, sTarget, 0); if(fAddingTurningCost && fAddingRaiseGunCost)//dnl ch71 180913 { if(usRaiseGunCost > usTurningCost) @@ -344,27 +456,31 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) else usRaiseGunCost = 0; } - ubRawAPCost = MinAPsToShootOrStab(pSoldier, pOpponent->sGridNo, 0, FALSE, 2); + ubRawAPCost = MinAPsToShootOrStab(pSoldier, sTarget, 0, FALSE, 2); ubMinAPcost = ubRawAPCost + usTurningCost + sStanceAPcost + usRaiseGunCost; + if(pSoldier->bActionPoints - ubMinAPcost >= 0) { // calc next attack's minimum shooting cost (excludes readying & turning & raise gun) - ubMaxPossibleAimTime = CalcAimingLevelsAvailableWithAP(pSoldier, pOpponent->sGridNo, pSoldier->bActionPoints-ubMinAPcost); + ubMaxPossibleAimTime = CalcAimingLevelsAvailableWithAP(pSoldier, sTarget, pSoldier->bActionPoints - ubMinAPcost); // sevenfm: check CTGT and friendly fire chance for every stance gUnderFire.Clear(); gUnderFire.Enable(); - ubChanceToGetThrough = AISoldierToSoldierChanceToGetThrough(pSoldier, pOpponent); + 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 (ubAimTime = APBPConstants[AP_MIN_AIM_ATTACK]; ubAimTime <= ubMaxPossibleAimTime; ubAimTime++) + for (ubAimTime = 0; ubAimTime <= ubMaxPossibleAimTime; ubAimTime++) { - ubChanceToHit = AICalcChanceToHitGun(pSoldier, pOpponent->sGridNo, ubAimTime, AIM_SHOT_TORSO, pOpponent->pathing.bLevel, STANDING); - iHitRate = ((pSoldier->bActionPoints - (ubMinAPcost - ubRawAPCost)) * ubChanceToHit) / (ubRawAPCost + ubAimTime * APBPConstants[AP_CLICK_AIM]); + ubChanceToHit = AICalcChanceToHitGun(pSoldier, sTarget, ubAimTime, AIM_SHOT_TORSO, bLevel, STANDING); + iHitRate = ubChanceToHit * (pSoldier->bActionPoints - (ubMinAPcost - ubRawAPCost)) / (ubRawAPCost + ubAimTime * APBPConstants[AP_CLICK_AIM]); // sevenfm: take into account CTGT for every stance if (iHitRate * ubChanceToGetThrough > iBestHitRate * ubBestChanceToGetThrough || (Item[pSoldier->usAttackingWeapon].usItemClass & IC_THROWING_KNIFE) && ubChanceToHit > ubBestChanceToHit)// rather take best chance for throwing knives @@ -385,12 +501,14 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) 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 ) || (Item[pSoldier->usAttackingWeapon].usItemClass & IC_THROWING_KNIFE) ) continue; // --------- Crouched --------- ubStance = ANIM_CROUCH; - if(IsValidStance(pSoldier, ubStance)) + // sevenfm: take into account direction + if (pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, sTarget), ubStance)) { // change stance then turn sStanceAPcost = GetAPsToChangeStance(pSoldier, ubStance); @@ -399,9 +517,9 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) pSoldier->usAnimState = CROUCHING; pSoldier->sLastTarget = NOWHERE; } - GetAPChargeForShootOrStabWRTGunRaises(pSoldier, pOpponent->sGridNo, TRUE, &fAddingTurningCost, &fAddingRaiseGunCost, 0); + GetAPChargeForShootOrStabWRTGunRaises(pSoldier, sTarget, TRUE, &fAddingTurningCost, &fAddingRaiseGunCost, 0); usTurningCost = CalculateTurningCost(pSoldier, pSoldier->usAttackingWeapon, fAddingTurningCost); - usRaiseGunCost = CalculateRaiseGunCost(pSoldier, fAddingRaiseGunCost, pOpponent->sGridNo, 0); + usRaiseGunCost = CalculateRaiseGunCost(pSoldier, fAddingRaiseGunCost, sTarget, 0); if(fAddingTurningCost && fAddingRaiseGunCost)//dnl ch71 180913 { if(usRaiseGunCost > usTurningCost) @@ -409,27 +527,30 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) else usRaiseGunCost = 0; } - ubRawAPCost = MinAPsToShootOrStab(pSoldier, pOpponent->sGridNo, 0, FALSE, 2); + ubRawAPCost = MinAPsToShootOrStab(pSoldier, sTarget, 0, FALSE, 2); ubMinAPcost = ubRawAPCost + usTurningCost + sStanceAPcost + usRaiseGunCost; if(pSoldier->bActionPoints - ubMinAPcost >= 0) { - ubMaxPossibleAimTime = CalcAimingLevelsAvailableWithAP(pSoldier, pOpponent->sGridNo, pSoldier->bActionPoints-ubMinAPcost); + ubMaxPossibleAimTime = CalcAimingLevelsAvailableWithAP(pSoldier, sTarget, pSoldier->bActionPoints - ubMinAPcost); // sevenfm: check CTGT and friendly fire chance for every stance gUnderFire.Clear(); gUnderFire.Enable(); - ubChanceToGetThrough = AISoldierToSoldierChanceToGetThrough(pSoldier, pOpponent); + 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 (ubAimTime = APBPConstants[AP_MIN_AIM_ATTACK]; ubAimTime <= ubMaxPossibleAimTime; ubAimTime++) + for (ubAimTime = 0; ubAimTime <= ubMaxPossibleAimTime; ubAimTime++) { - ubChanceToHit = AICalcChanceToHitGun(pSoldier, pOpponent->sGridNo, ubAimTime, AIM_SHOT_TORSO, pOpponent->pathing.bLevel, CROUCHING); - iHitRate = ((pSoldier->bActionPoints - (ubMinAPcost - ubRawAPCost)) * ubChanceToHit) / (ubRawAPCost + ubAimTime * APBPConstants[AP_CLICK_AIM]); + ubChanceToHit = AICalcChanceToHitGun(pSoldier, sTarget, ubAimTime, AIM_SHOT_TORSO, bLevel, CROUCHING); + iHitRate = ubChanceToHit * (pSoldier->bActionPoints - (ubMinAPcost - ubRawAPCost)) / (ubRawAPCost + ubAimTime * APBPConstants[AP_CLICK_AIM]); // sevenfm: take into account CTGT for every stance if (iHitRate * ubChanceToGetThrough > iBestHitRate * ubBestChanceToGetThrough) { @@ -450,7 +571,7 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) } // no prone stance if we have to change direction and stance at the same time - if (pSoldier->ubDirection != AIDirection(pSoldier->sGridNo, pOpponent->sGridNo) && + if (pSoldier->ubDirection != AIDirection(pSoldier->sGridNo, sTarget) && gAnimControl[pSoldier->usAnimState].ubEndHeight > ANIM_PRONE) { continue; @@ -458,8 +579,7 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) // --------- Prone --------- ubStance = ANIM_PRONE; - //if(IsValidStance(pSoldier, ubStance)) - if (pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, pOpponent->sGridNo), ubStance)) + if (pSoldier->InternalIsValidStance(AIDirection(pSoldier->sGridNo, sTarget), ubStance)) { sStanceAPcost = GetAPsToChangeStance(pSoldier, ubStance); if (sStanceAPcost) @@ -467,30 +587,33 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) pSoldier->usAnimState = PRONE; pSoldier->sLastTarget = NOWHERE; } - GetAPChargeForShootOrStabWRTGunRaises(pSoldier, pOpponent->sGridNo, TRUE, &fAddingTurningCost, &fAddingRaiseGunCost, 0); + GetAPChargeForShootOrStabWRTGunRaises(pSoldier, sTarget, TRUE, &fAddingTurningCost, &fAddingRaiseGunCost, 0); usTurningCost = CalculateTurningCost(pSoldier, pSoldier->usAttackingWeapon, fAddingTurningCost); - usRaiseGunCost = CalculateRaiseGunCost(pSoldier, fAddingRaiseGunCost, pOpponent->sGridNo, 0); - ubRawAPCost = MinAPsToShootOrStab(pSoldier, pOpponent->sGridNo, 0, FALSE, 2); + usRaiseGunCost = CalculateRaiseGunCost(pSoldier, fAddingRaiseGunCost, sTarget, 0); + ubRawAPCost = MinAPsToShootOrStab(pSoldier, sTarget, 0, FALSE, 2); ubMinAPcost = ubRawAPCost + usTurningCost + sStanceAPcost + usRaiseGunCost; - if(pSoldier->bActionPoints - ubMinAPcost >= 0) + if (pSoldier->bActionPoints - ubMinAPcost >= 0) { - ubMaxPossibleAimTime = CalcAimingLevelsAvailableWithAP(pSoldier, pOpponent->sGridNo, pSoldier->bActionPoints-ubMinAPcost); + ubMaxPossibleAimTime = CalcAimingLevelsAvailableWithAP(pSoldier, sTarget, pSoldier->bActionPoints - ubMinAPcost); // sevenfm: check CTGT and friendly fire chance for every stance gUnderFire.Clear(); gUnderFire.Enable(); - ubChanceToGetThrough = AISoldierToSoldierChanceToGetThrough(pSoldier, pOpponent); + 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 (ubAimTime = APBPConstants[AP_MIN_AIM_ATTACK]; ubAimTime <= ubMaxPossibleAimTime; ubAimTime++) + for (ubAimTime = 0; ubAimTime <= ubMaxPossibleAimTime; ubAimTime++) { - ubChanceToHit = AICalcChanceToHitGun(pSoldier, pOpponent->sGridNo, ubAimTime, AIM_SHOT_TORSO, pOpponent->pathing.bLevel, PRONE); - iHitRate = ((pSoldier->bActionPoints - (ubMinAPcost - ubRawAPCost)) * ubChanceToHit) / (ubRawAPCost + ubAimTime * APBPConstants[AP_CLICK_AIM]); + ubChanceToHit = AICalcChanceToHitGun(pSoldier, sTarget, ubAimTime, AIM_SHOT_TORSO, bLevel, PRONE); + iHitRate = ubChanceToHit * (pSoldier->bActionPoints - (ubMinAPcost - ubRawAPCost)) / (ubRawAPCost + ubAimTime * APBPConstants[AP_CLICK_AIM]); // sevenfm: take into account CTGT for every stance if (iHitRate * ubChanceToGetThrough > iBestHitRate * ubBestChanceToGetThrough) { @@ -524,14 +647,15 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) 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, pOpponent->sGridNo ) > CalcMaxTossRange( pSoldier, pSoldier->usAttackingWeapon, FALSE ) )))// Madd / 2 ) ) ) //dnl ch69 160913 was ubChanceToReallyHit < 30 + 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); + iThreatValue = CalcManThreatValue(pOpponent, pSoldier->sGridNo, TRUE, pSoldier); // estimate the damage this shot would do to this opponent - iEstDamage = EstimateShotDamage(pSoldier,pOpponent,ubBestChanceToHit); + iEstDamage = EstimateShotDamage(pSoldier, pOpponent, ubBestChanceToHit); //NumMessage("SHOT EstDamage = ",iEstDamage); // calculate the combined "attack value" for this opponent @@ -540,6 +664,13 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) 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 ) { @@ -624,8 +755,8 @@ void CalcBestShot(SOLDIERTYPE *pSoldier, ATTACKTYPE *pBestShot) pBestShot->ubOpponent = pOpponent->ubID; pBestShot->ubAimTime = ubBestAimTime; pBestShot->ubChanceToReallyHit = ubChanceToReallyHit; - pBestShot->sTarget = pOpponent->sGridNo; - pBestShot->bTargetLevel = pOpponent->pathing.bLevel; + pBestShot->sTarget = sTarget; + pBestShot->bTargetLevel = bLevel; pBestShot->iAttackValue = iAttackValue; pBestShot->ubAPCost = sBestAPcost; pBestShot->ubStance = ubBestStance; diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index f11ef71d..4871e21d 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -2910,7 +2910,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) } //SUPPRESSION FIRE - CheckIfShotPossible(pSoldier,&BestShot); //WarmSteel - No longer returns 0 when there IS actually a chance to hit. + CheckIfShotPossible(pSoldier, &BestShot); //WarmSteel - No longer returns 0 when there IS actually a chance to hit. // sevenfm: check that we have a clip to reload BOOLEAN fExtraClip = FALSE; @@ -2929,77 +2929,131 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) // 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.bWeaponIn != -1 - && BestShot.ubPossible - && GetMagSize(&pSoldier->inv[BestShot.bWeaponIn]) >= gGameExternalOptions.ubAISuppressionMinimumMagSize - && pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft >= gGameExternalOptions.ubAISuppressionMinimumAmmo - //&& BestShot.ubChanceToReallyHit < (INT16)(PreRandom(50)) - //&& Menptr[BestShot.ubOpponent].pathing.bLevel == 0 - && pSoldier->aiData.bOrders != SNIPER && - BestShot.ubFriendlyFireChance < 5 && - !CoweringShockLevel(MercPtrs[BestShot.ubOpponent]) && + if (BestShot.ubPossible && + BestShot.bWeaponIn != -1 && + // check valid target + !TileIsOutOfBounds(BestShot.sTarget) && + BestShot.ubOpponent != NOBODY && + MercPtrs[BestShot.ubOpponent] && + // 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) && - LocationToLocationLineOfSightTest( pSoldier->sGridNo, pSoldier->pathing.bLevel, MercPtrs[BestShot.ubOpponent]->sGridNo, MercPtrs[BestShot.ubOpponent]->pathing.bLevel, TRUE, NO_DISTANCE_LIMIT) && - //Weapon[pSoldier->inv[BestShot.bWeaponIn].usItem].ubWeaponType == GUN_LMG ) && //Weapon[usInHand].ubWeaponClass == MGCLASS - (fExtraClip || pSoldier->inv[BestShot.bWeaponIn][0]->data.gun.ubGunShotsLeft > gGameExternalOptions.ubAISuppressionMinimumMagSize) ) + // reduce chance to shoot if target is beyond weapon range + (AICheckIsMachinegunner(pSoldier) || + ARMED_VEHICLE(pSoldier) || + AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || + pSoldier->aiData.bUnderFire && (pSoldier->ubPreviousAttackerID == BestShot.ubOpponent || pSoldier->ubNextToPreviousAttackerID == BestShot.ubOpponent || MercPtrs[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 - DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: suppression fire possible!"); + DebugAI(AI_MSG_INFO, pSoldier, String("suppression fire possible! target %d level %d aim %d", BestShot.sTarget, BestShot.bTargetLevel, BestShot.ubAimTime)); if (BestShot.bWeaponIn != HANDPOS) { - RearrangePocket(pSoldier,HANDPOS,BestShot.bWeaponIn,FOREVER); + DebugAI(AI_MSG_INFO, pSoldier, String("rearrange pocket")); + RearrangePocket(pSoldier, HANDPOS, BestShot.bWeaponIn, FOREVER); } - pSoldier->aiData.usActionData = BestShot.sTarget; - pSoldier->bTargetLevel = BestShot.bTargetLevel; - pSoldier->aiData.bAimTime = 0; - pSoldier->bDoAutofire = 0; - pSoldier->bDoBurst = 1; + 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(UsingNewCTHSystem() == true) + if (BestShot.ubAimTime > 0 && + !UsingNewCTHSystem() && + Chance((100 - BestShot.ubChanceToReallyHit) * (100 - BestShot.ubChanceToReallyHit) / 100)) { - 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 && pSoldier->inv[ pSoldier->ubAttackingHand ][0]->data.gun.ubGunShotsLeft >= pSoldier->bDoAutofire - && dTotalRecoil <= 10.0f ); + DebugAI(AI_MSG_INFO, pSoldier, String("set ubAimTime = 0 for OCTH suppression")); + BestShot.ubAimTime = 0; } - else + + // reserve APs to hide if no cover or enemy is close + if (!AnyCoverAtSpot(pSoldier, pSoldier->sGridNo) || PythSpacesAway(pSoldier->sGridNo, BestShot.sTarget) < DAY_VISION_RANGE / 2) + { + sReserveAP = APBPConstants[AP_MINIMUM] / 2; + } + if (PythSpacesAway(pSoldier->sGridNo, BestShot.sTarget) > DAY_VISION_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++; - ubBurstAPs = CalcAPsToAutofire( pSoldier->CalcActionPoints(), &(pSoldier->inv[BestShot.bWeaponIn]), pSoldier->bDoAutofire, pSoldier ); - } - while( pSoldier->bActionPoints >= BestShot.ubAPCost + ubBurstAPs && - pSoldier->inv[ pSoldier->ubAttackingHand ][0]->data.gun.ubGunShotsLeft >= pSoldier->bDoAutofire && - GetAutoPenalty(&pSoldier->inv[ pSoldier->ubAttackingHand ], gAnimControl[ pSoldier->usAnimState ].ubEndHeight == ANIM_PRONE)*pSoldier->bDoAutofire <= 80 ); + 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 ); + 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)); - // minimum 5 bullets - if (pSoldier->bDoAutofire >= 5 && pSoldier->bActionPoints >= BestShot.ubAPCost + ubBurstAPs ) + // minimum 3 bullets + if (pSoldier->bDoAutofire >= 3 && pSoldier->bActionPoints >= BestShot.ubAPCost + sActualAimAP + ubBurstAPs + sReserveAP) { - ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, New113Message[ MSG113_SUPPRESSIONFIRE ] ); - return( AI_ACTION_FIRE_GUN ); + 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")); + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, New113Message[MSG113_SUPPRESSIONFIRE]); + return(AI_ACTION_CHANGE_STANCE); + } + else + { + pSoldier->aiData.usActionData = BestShot.sTarget; + + ScreenMsg(FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, New113Message[MSG113_SUPPRESSIONFIRE]); + return(AI_ACTION_FIRE_GUN); + } } else { - pSoldier->bDoBurst = 0; - pSoldier->bDoAutofire = 0; + pSoldier->bDoBurst = 0; + pSoldier->bDoAutofire = 0; } } // suppression not possible, do something else diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index 747d03fa..82dab47d 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -330,15 +330,14 @@ UINT8 TerrainDensity(INT32 sSpot, INT8 bLevel, UINT8 ubDistance, BOOLEAN fGrass) BOOLEAN FindNearbyExplosiveStructure(INT32 sSpot, INT8 bLevel); INT16 DistanceToClosestActiveOpponent(SOLDIERTYPE *pSoldier, INT32 sSpot); BOOLEAN ValidOpponent(SOLDIERTYPE* pSoldier, SOLDIERTYPE* pOpponent); - UINT8 CountSeenEnemiesLastTurn( SOLDIERTYPE *pSoldier ); - BOOLEAN FindObstacleNearSpot(INT32 sSpot, INT8 bLevel); - BOOLEAN CheckNPCDestination(SOLDIERTYPE *pSoldier, INT32 sGridNo); UINT8 SpotDangerLevel(SOLDIERTYPE *pSoldier, INT32 sGridNo); BOOLEAN AllowDeepWaterFlanking(SOLDIERTYPE *pSoldier); BOOLEAN AICheckUnderground(void); +INT32 RandomizeLocation(INT32 sSpot, INT8 bLevel, UINT8 ubTimes, SOLDIERTYPE *pSightSoldier); +INT32 RandomizeOpponentLocation(INT32 sSpot, SOLDIERTYPE *pOpponent, INT16 sMaxDistance); BOOLEAN CorpseWarning(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bLevel); BOOLEAN CorpseEnemyTeam(ROTTING_CORPSE *pCorpse);