#include "ai.h" #include "Weapons.h" #include "opplist.h" #include "Points.h" #include "PATHAI.H" #include "worldman.h" #include "AIInternals.h" #include "Items.h" #include "message.h" #include "LOS.h" #include "Assignments.h" #include "Soldier Functions.h" #include "Points.h" #include "GameSettings.h" #include "Buildings.h" #include "Soldier macros.h" #include "Render Fun.h" #include "strategicmap.h" #include "environment.h" #include "lighting.h" #include "Soldier Create.h" #include "SkillCheck.h" // added by SANDRO #include "Vehicles.h" // added by silversurfer #include "Game Clock.h" // sevenfm #include "Rotting Corpses.h" // sevenfm #include "WCheck.h" // sevenfm #include "SmokeEffects.h" // sevenfm #include "GameInitOptionsScreen.h" ////////////////////////////////////////////////////////////////////////////// // SANDRO - In this file, all APBPConstants[AP_CROUCH] and APBPConstants[AP_PRONE] were changed to GetAPsCrouch() and GetAPsProne() // On the bottom here, there are these functions made ////////////////////////////////////////////////////////////////////// // // CJC's DG->JA2 conversion notes // // Commented out: // // InWaterOrGas - gas stuff // RoamingRange - point patrol stuff // sevenfm extern UINT16 PickSoldierReadyAnimation(SOLDIERTYPE *pSoldier, BOOLEAN fEndReady, BOOLEAN fHipStance); extern SECTOR_EXT_DATA SectorExternalData[256][4]; UINT8 Urgency[NUM_STATUS_STATES][NUM_MORALE_STATES] = { {URGENCY_LOW, URGENCY_LOW, URGENCY_LOW, URGENCY_LOW, URGENCY_LOW}, // green {URGENCY_HIGH, URGENCY_MED, URGENCY_MED, URGENCY_LOW, URGENCY_LOW}, // yellow {URGENCY_HIGH, URGENCY_MED, URGENCY_MED, URGENCY_MED, URGENCY_MED}, // red {URGENCY_HIGH, URGENCY_HIGH, URGENCY_HIGH, URGENCY_MED, URGENCY_MED} // black }; UINT16 MovementMode[LAST_MOVEMENT_ACTION + 1][NUM_URGENCY_STATES] = { {WALKING, WALKING, WALKING }, // AI_ACTION_NONE {WALKING, WALKING, WALKING }, // AI_ACTION_RANDOM_PATROL {WALKING, RUNNING, RUNNING }, // AI_ACTION_SEEK_FRIEND {WALKING, RUNNING, RUNNING }, // AI_ACTION_SEEK_OPPONENT {RUNNING, RUNNING, RUNNING }, // AI_ACTION_TAKE_COVER {WALKING, RUNNING, RUNNING }, // AI_ACTION_GET_CLOSER {WALKING, WALKING, WALKING }, // AI_ACTION_POINT_PATROL, {WALKING, RUNNING, RUNNING }, // AI_ACTION_LEAVE_WATER_GAS, {WALKING, SWATTING, RUNNING }, // AI_ACTION_SEEK_NOISE, {RUNNING, RUNNING, RUNNING }, // AI_ACTION_ESCORTED_MOVE, {WALKING, RUNNING, RUNNING }, // AI_ACTION_RUN_AWAY, {RUNNING, RUNNING, RUNNING }, // AI_ACTION_KNIFE_MOVE {WALKING, WALKING, WALKING }, // AI_ACTION_APPROACH_MERC {RUNNING, RUNNING, RUNNING }, // AI_ACTION_TRACK {RUNNING, RUNNING, RUNNING }, // AI_ACTION_EAT {WALKING, RUNNING, RUNNING}, // AI_ACTION_PICKUP_ITEM {WALKING, WALKING, WALKING}, // AI_ACTION_SCHEDULE_MOVE {WALKING, WALKING, WALKING}, // AI_ACTION_WALK {WALKING, RUNNING, RUNNING}, // withdraw {RUNNING, RUNNING, SWATTING}, // flank left {RUNNING, RUNNING, SWATTING}, // flank right {RUNNING, RUNNING, RUNNING}, // AI_ACTION_MOVE_TO_CLIMB }; INT8 OKToAttack(SOLDIERTYPE * pSoldier, int target) { // can't shoot yourself if (target == pSoldier->sGridNo) return(NOSHOOT_MYSELF); if (WaterTooDeepForAttacks(pSoldier->sGridNo, pSoldier->pathing.bLevel)) return(NOSHOOT_WATER); // make sure a weapon is in hand (FEB.8 ADDITION: tossable items are also OK) if (!WeaponInHand(pSoldier)) { return(NOSHOOT_NOWEAPON); } // JUST PUT THIS IN ON JULY 13 TO TRY AND FIX OUT-OF-AMMO SITUATIONS if ( Item[pSoldier->inv[HANDPOS].usItem].usItemClass == IC_GUN) { if (ItemIsCannon(pSoldier->inv[HANDPOS].usItem)) { // look for another tank shell ELSEWHERE IN INVENTORY if ( FindLaunchable( pSoldier, pSoldier->inv[HANDPOS].usItem ) == NO_SLOT ) //if ( !ItemHasAttachments( &(pSoldier->inv[HANDPOS]) ) ) { return(NOSHOOT_NOLOAD); } } else if (pSoldier->inv[HANDPOS][0]->data.gun.ubGunShotsLeft == 0 /*SB*/ || !(pSoldier->inv[HANDPOS][0]->data.gun.ubGunState & GS_CARTRIDGE_IN_CHAMBER) || (pSoldier->IsValidSecondHandShotForReloadingPurposes( ) && (pSoldier->inv[SECONDHANDPOS][0]->data.gun.ubGunShotsLeft == 0 || !(pSoldier->inv[SECONDHANDPOS][0]->data.gun.ubGunState & GS_CARTRIDGE_IN_CHAMBER)))) { return(NOSHOOT_NOAMMO); } } else if (Item[pSoldier->inv[HANDPOS].usItem].usItemClass == IC_LAUNCHER) { if ( FindLaunchable( pSoldier, pSoldier->inv[HANDPOS].usItem ) == NO_SLOT ) //if ( !ItemHasAttachments( &(pSoldier->inv[HANDPOS]) ) ) { return(NOSHOOT_NOLOAD); } } return(TRUE); } BOOLEAN ConsiderProne( SOLDIERTYPE * pSoldier ) { INT32 sOpponentGridNo; INT8 bOpponentLevel; 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, nullptr, &iRangeInCellCoords); if ( iRangeInCellCoords > 10*CELL_X_SIZE) { return( TRUE ); } else { return( FALSE ); } } UINT8 StanceChange( SOLDIERTYPE * pSoldier, INT16 ubAttackAPCost ) { // consider crouching or going prone if (PTR_STANDING) { if (pSoldier->bActionPoints - ubAttackAPCost >= GetAPsCrouch(pSoldier, TRUE)) { if ( (pSoldier->bActionPoints - ubAttackAPCost >= GetAPsCrouch(pSoldier, TRUE) + GetAPsProne(pSoldier, TRUE)) && IsValidStance( pSoldier, ANIM_PRONE ) && ConsiderProne( pSoldier ) ) { return( ANIM_PRONE ); } else if ( IsValidStance( pSoldier, ANIM_CROUCH ) ) { return( ANIM_CROUCH ); } } } else if (PTR_CROUCHED) { if ( (pSoldier->bActionPoints - ubAttackAPCost >= GetAPsProne(pSoldier, TRUE)) && IsValidStance( pSoldier, ANIM_PRONE ) && ConsiderProne( pSoldier ) ) { return( ANIM_PRONE ); } } return( 0 ); } UINT8 ShootingStanceChange( SOLDIERTYPE * pSoldier, ATTACKTYPE * pAttack, INT8 bDesiredDirection ) { // Figure out the best stance for this attack // We don't want to go through a lot of complex calculations here, // just compare the chance of the bullet hitting if we are // standing, crouched, or prone UINT16 usRealAnimState, usBestAnimState; INT8 bBestStanceDiff=-1; INT8 bLoop, bStanceNum, bStanceDiff, bAPsAfterAttack, bCurAimTime, bSetScopeMode; UINT32 uiChanceOfDamage, uiBestChanceOfDamage, uiCurrChanceOfDamage; UINT32 uiStanceBonus, uiMinimumStanceBonusPerChange = 20 - 3 * pAttack->ubAimTime; INT32 iRange; bStanceNum = 0; uiCurrChanceOfDamage = 0; bSetScopeMode = pSoldier->bScopeMode; pSoldier->bScopeMode = pAttack->bScopeMode; bAPsAfterAttack = pSoldier->bActionPoints - MinAPsToAttack( pSoldier, pAttack->sTarget, ADDTURNCOST, pAttack->ubAimTime, 1); pSoldier->bScopeMode = bSetScopeMode; if (bAPsAfterAttack < GetAPsCrouch(pSoldier, TRUE)) { return( 0 ); } // Unfortunately, to get this to work, we have to fake the AI guy's // animation state so we get the right height values usRealAnimState = pSoldier->usAnimState; usBestAnimState = pSoldier->usAnimState; uiBestChanceOfDamage = 0; iRange = GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, pAttack->sTarget ); switch( gAnimControl[usRealAnimState].ubEndHeight ) { // set a stance number comparable with our loop variable so we can easily compute // stance differences and thus AP cost case ANIM_STAND: bStanceNum = 0; break; case ANIM_CROUCH: bStanceNum = 1; break; case ANIM_PRONE: bStanceNum = 2; break; } for (bLoop = 0; bLoop < 3; bLoop++) { bStanceDiff = abs( bLoop - bStanceNum ); if (bStanceDiff == 2 && bAPsAfterAttack < GetAPsCrouch(pSoldier, TRUE) + GetAPsProne(pSoldier, TRUE)) { // can't consider this! continue; } switch( bLoop ) { case 0: if ( !pSoldier->InternalIsValidStance( bDesiredDirection, ANIM_STAND ) ) { continue; } pSoldier->usAnimState = STANDING; break; case 1: if ( !pSoldier->InternalIsValidStance( bDesiredDirection, ANIM_CROUCH ) ) { continue; } pSoldier->usAnimState = CROUCHING; break; default: if ( !pSoldier->InternalIsValidStance( bDesiredDirection, ANIM_PRONE ) ) { continue; } pSoldier->usAnimState = PRONE; break; } // Hack: Assumes the cost to reach the target stance from the current stance is the same as going back. Probably true. bCurAimTime = __min( bAPsAfterAttack - GetAPsToChangeStance( pSoldier, bStanceNum), pAttack->ubAimTime); // If can't fire at all from this stance, don't bother with the chances of hitting if (bCurAimTime < 0) { continue; } uiChanceOfDamage = SoldierToLocationChanceToGetThrough( pSoldier, pAttack->sTarget, pSoldier->bTargetLevel, pSoldier->bTargetCubeLevel, pAttack->ubOpponent ) * CalcChanceToHitGun( pSoldier, pAttack->sTarget, bCurAimTime, AIM_SHOT_TORSO ) / 100; if (uiChanceOfDamage > 0) { uiStanceBonus = 0; // artificially augment "chance of damage" to reflect penalty to be shot at various stances switch( pSoldier->usAnimState ) { case CROUCHING: if (iRange > POINT_BLANK_RANGE + 10 * (AIM_PENALTY_TARGET_CROUCHED / 3)) { uiStanceBonus = AIM_BONUS_CROUCHING; } else if (iRange > POINT_BLANK_RANGE) { // reduce chance to hit with distance to the prone/immersed target uiStanceBonus = 3 * ((iRange - POINT_BLANK_RANGE) / CELL_X_SIZE); // penalty -3%/tile } break; case PRONE: if (iRange <= MIN_PRONE_RANGE) { // HATE being prone this close! uiChanceOfDamage = 0; } else //if (iRange > POINT_BLANK_RANGE) { // reduce chance to hit with distance to the prone/immersed target uiStanceBonus = 3 * ((iRange - POINT_BLANK_RANGE) / CELL_X_SIZE); // penalty -3%/tile } break; default: break; } // reduce stance bonus according to how much we have to change stance to get there //uiStanceBonus = uiStanceBonus * (4 - bStanceDiff) / 4; uiChanceOfDamage += uiStanceBonus; } if (bStanceDiff == 0) { uiCurrChanceOfDamage = uiChanceOfDamage; } if (uiChanceOfDamage > uiBestChanceOfDamage ) { uiBestChanceOfDamage = uiChanceOfDamage; usBestAnimState = pSoldier->usAnimState; bBestStanceDiff = bStanceDiff; } } pSoldier->usAnimState = usRealAnimState; // return 0 or the best height value to be at if (bBestStanceDiff == 0 || ((uiBestChanceOfDamage - uiCurrChanceOfDamage) / bBestStanceDiff) < uiMinimumStanceBonusPerChange) { // better off not changing our stance! return( 0 ); } else { return( gAnimControl[ usBestAnimState ].ubEndHeight ); } } UINT16 DetermineMovementMode( SOLDIERTYPE * pSoldier, INT8 bAction ) { if ( pSoldier->IsFastMovement() ) { return( RUNNING ); } else if ( CREATURE_OR_BLOODCAT( pSoldier ) ) { if (pSoldier->aiData.bAlertStatus == STATUS_GREEN) return( WALKING ); else return( RUNNING ); } // zombies always run if they know enemy location else if (gGameExternalOptions.fAIMovementMode && pSoldier->IsZombie() && IS_MERC_BODY_TYPE(pSoldier)) { if (!TileIsOutOfBounds(ClosestKnownOpponent(pSoldier, NULL, NULL))) return RUNNING; else return WALKING; } else if (pSoldier->ubBodyType == COW || pSoldier->ubBodyType == CROW) { return( WALKING ); } else { if ( (pSoldier->aiData.fAIFlags & AI_CAUTIOUS) && (MovementMode[bAction][Urgency[pSoldier->aiData.bAlertStatus][pSoldier->aiData.bAIMorale]] == RUNNING) ) { return( WALKING ); } // if soldier is already crouched/prone, use SWATTING if ((pSoldier->aiData.fAIFlags & AI_CAUTIOUS) && gGameExternalOptions.fAIMovementMode && IS_MERC_BODY_TYPE(pSoldier) && (pSoldier->bTeam == ENEMY_TEAM || pSoldier->bTeam == MILITIA_TEAM) && gAnimControl[pSoldier->usAnimState].ubEndHeight <= ANIM_CROUCH) { return SWATTING; } if ((pSoldier->aiData.fAIFlags & AI_CAUTIOUS) && (MovementMode[bAction][Urgency[pSoldier->aiData.bAlertStatus][pSoldier->aiData.bAIMorale]] == RUNNING)) { return(WALKING); } else if ( bAction == AI_ACTION_SEEK_NOISE && pSoldier->bTeam == CIV_TEAM && !IS_MERC_BODY_TYPE( pSoldier ) ) { return( WALKING ); } else if ( (pSoldier->ubBodyType == HATKIDCIV || pSoldier->ubBodyType == KIDCIV) && (pSoldier->aiData.bAlertStatus == STATUS_GREEN) && Random( 10 ) == 0 ) { return( KID_SKIPPING ); } else { // sevenfm: movement mode tweaks if (gGameExternalOptions.fAIMovementMode) { 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 && TileIsOutOfBounds(sClosestThreat) && !pSoldier->aiData.bUnderFire && (bAction == AI_ACTION_SEEK_FRIEND || bAction == AI_ACTION_SEEK_NOISE || bAction == AI_ACTION_TAKE_COVER)) { return WALKING; } // use swatting when blinded if (IS_MERC_BODY_TYPE(pSoldier) && pSoldier->bBlindedCounter > 0) { return SWATTING; } if (IS_MERC_BODY_TYPE(pSoldier) && pSoldier->aiData.bAlertStatus >= STATUS_YELLOW && !InWaterGasOrSmoke(pSoldier, pSoldier->sGridNo) && !BOXER(pSoldier) && !TileIsOutOfBounds(sClosestThreat) && (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 if (NightTime() && InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel) && (bAction == AI_ACTION_SEEK_OPPONENT || bAction == AI_ACTION_GET_CLOSER || bAction == AI_ACTION_SEEK_FRIEND || bAction == AI_ACTION_TAKE_COVER)) { return RUNNING; } // use swatting for seeking at night or when soldier is already crouched if (!InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel) && pSoldier->aiData.bAlertStatus == STATUS_RED && iRCD < 4 && !pSoldier->aiData.bUnderFire && pSoldier->aiData.bShock == 0 && !GuySawEnemy(pSoldier) && (NightTime() || gAnimControl[pSoldier->usAnimState].ubEndHeight <= ANIM_CROUCH) && CountNearbyFriends(pSoldier, pSoldier->sGridNo, TACTICAL_RANGE / 4) < 3 && distanceToThreat < beyondVisionRange && CountFriendsBlack(pSoldier) == 0 && bAction == AI_ACTION_SEEK_OPPONENT) { return SWATTING; } // use swatting for taking cover if (pSoldier->aiData.bAlertStatus >= STATUS_RED && distanceToThreat > reallyClose && (pSoldier->aiData.bUnderFire && iRCD < 4 || pSoldier->aiData.bShock > 2 * iRCD || pSoldier->aiData.bShock > 0 && gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_PRONE) && !pSoldier->aiData.bLastAttackHit && bAction == AI_ACTION_TAKE_COVER) { return SWATTING; } // use SWATTING when under fire if (pSoldier->aiData.bAlertStatus >= STATUS_RED && (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 || bAction == AI_ACTION_GET_CLOSER || bAction == AI_ACTION_SEEK_FRIEND || bAction == AI_ACTION_TAKE_COVER)) { return SWATTING; } // use SWATTING when in a room and seen enemy recently or under fire if (InARoom(pSoldier->sGridNo, NULL) && pSoldier->aiData.bAlertStatus >= STATUS_YELLOW && (pSoldier->aiData.bOrders == SNIPER || pSoldier->aiData.bOrders == STATIONARY || (GuySawEnemy(pSoldier) || pSoldier->aiData.bShock > 0) && iRCD < 4) && distanceToThreat > close && (bAction == AI_ACTION_SEEK_OPPONENT || bAction == AI_ACTION_GET_CLOSER || bAction == AI_ACTION_SEEK_FRIEND || bAction == AI_ACTION_TAKE_COVER || bAction == AI_ACTION_SEEK_NOISE)) { return SWATTING; } // use swatting for snipers on roof or when under fire if (pSoldier->pathing.bLevel > 0 && pSoldier->aiData.bAlertStatus >= STATUS_YELLOW && (pSoldier->aiData.bOrders == SNIPER || pSoldier->aiData.bOrders == STATIONARY || pSoldier->aiData.bShock > 0 && iRCD < 4) && distanceToThreat > close && (bAction == AI_ACTION_SEEK_OPPONENT || bAction == AI_ACTION_GET_CLOSER || bAction == AI_ACTION_SEEK_FRIEND || bAction == AI_ACTION_TAKE_COVER || bAction == AI_ACTION_SEEK_NOISE)) { return SWATTING; } // use running for taking cover when not under attack if (!pSoldier->aiData.bUnderFire && bAction == AI_ACTION_TAKE_COVER && pSoldier->bInitialActionPoints > APBPConstants[AP_MINIMUM] && (!InARoom(pSoldier->sGridNo, NULL) || distanceToThreat > sDistanceVisible * 20) && pSoldier->aiData.bAIMorale >= MORALE_NORMAL && pSoldier->bBreath > 25 && pSoldier->pathing.bLevel == 0 && pSoldier->aiData.bOrders != STATIONARY && pSoldier->aiData.bOrders != SNIPER && (gAnimControl[pSoldier->usAnimState].ubEndHeight > ANIM_PRONE || pSoldier->bActionPoints > APBPConstants[AP_MINIMUM])) { return RUNNING; } // decide movement mode when getting closer if (bAction == AI_ACTION_GET_CLOSER) { if (gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_STAND) { if (WeaponReady(pSoldier) && !pSoldier->aiData.bUnderFire && pSoldier->aiData.bAlertStatus == STATUS_BLACK) return WALKING; else return RUNNING; } else if (gAnimControl[pSoldier->usAnimState].ubEndHeight == ANIM_CROUCH) { if (WeaponReady(pSoldier) && !pSoldier->aiData.bUnderFire && pSoldier->aiData.bAlertStatus == STATUS_BLACK || 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 && distanceToThreat > reallyClose ) return SWATTING; else return RUNNING; } } // use walking/swatting when flanking in realtime if (AICheckIsFlanking(pSoldier) && !gfTurnBasedAI) { if (NightTime()) return SWATTING; if (pSoldier->bBreath < pSoldier->bBreathMax / 2) return WALKING; } } } return( MovementMode[bAction][Urgency[pSoldier->aiData.bAlertStatus][pSoldier->aiData.bAIMorale]] ); } } } void NewDest(SOLDIERTYPE *pSoldier, INT32 usGridNo) { // sevenfm: always use DetermineMovementMode with new code if (gGameExternalOptions.fAIMovementMode) { pSoldier->usUIMovementMode = DetermineMovementMode(pSoldier, pSoldier->aiData.bAction); // check for non merc bodytypes if ((pSoldier->usUIMovementMode == SWATTING || pSoldier->usUIMovementMode == SWATTING_WK) && !IS_MERC_BODY_TYPE(pSoldier)) { pSoldier->usUIMovementMode = WALKING; } } else { // ATE: Setting sDestination? Tis does not make sense... //pSoldier->pathing.sDestination = usGridNo; BOOLEAN fSet = FALSE; if (IS_MERC_BODY_TYPE(pSoldier) && pSoldier->aiData.bAction == AI_ACTION_TAKE_COVER && (pSoldier->aiData.bAttitude == DEFENSIVE || pSoldier->aiData.bAttitude == CUNNINGSOLO || pSoldier->aiData.bAttitude == CUNNINGAID) && (SoldierDifficultyLevel(pSoldier) >= 2)) { UINT16 usMovementMode; // getting real movement anim for someone who is going to take cover, not just considering usMovementMode = MovementMode[AI_ACTION_TAKE_COVER][Urgency[pSoldier->aiData.bAlertStatus][pSoldier->aiData.bAIMorale]]; if (usMovementMode != SWATTING) { // really want to look at path, see how far we could get on path while swatting if (EnoughPoints(pSoldier, RecalculatePathCost(pSoldier, SWATTING), 0, FALSE) || (pSoldier->aiData.bLastAction == AI_ACTION_TAKE_COVER && pSoldier->usUIMovementMode == SWATTING)) { pSoldier->usUIMovementMode = SWATTING; } else { pSoldier->usUIMovementMode = usMovementMode; } } else { pSoldier->usUIMovementMode = usMovementMode; } fSet = TRUE; } else { if (pSoldier->bTeam == ENEMY_TEAM && pSoldier->aiData.bAlertStatus == STATUS_RED) { switch (pSoldier->aiData.bAction) { case AI_ACTION_MOVE_TO_CLIMB: case AI_ACTION_RUN_AWAY: pSoldier->usUIMovementMode = DetermineMovementMode(pSoldier, pSoldier->aiData.bAction); fSet = TRUE; break; default: if (!fSet) { pSoldier->usUIMovementMode = DetermineMovementMode(pSoldier, pSoldier->aiData.bAction); fSet = TRUE; } break; } } else { pSoldier->usUIMovementMode = DetermineMovementMode(pSoldier, pSoldier->aiData.bAction); fSet = TRUE; } if (pSoldier->usUIMovementMode == SWATTING && !IS_MERC_BODY_TYPE(pSoldier)) { pSoldier->usUIMovementMode = WALKING; } } } //pSoldier->EVENT_GetNewSoldierPath( pSoldier->pathing.sDestination, pSoldier->usUIMovementMode ); // ATE: Using this more versatile version // Last parameter says whether to re-start the soldier's animation // This should be done if buddy was paused for fNoApstofinishMove... pSoldier->EVENT_InternalGetNewSoldierPath( usGridNo, pSoldier->usUIMovementMode , FALSE, pSoldier->flags.fNoAPToFinishMove ); } BOOLEAN IsActionAffordable(SOLDIERTYPE *pSoldier, INT8 bAction) { INT16 bMinPointsNeeded = 0; //NumMessage("AffordableAction - Guy#",pSoldier->ubID); if( bAction == AI_ACTION_NONE ) { bAction = pSoldier->aiData.bAction; } switch (bAction) { case AI_ACTION_NONE: // maintain current position & facing // no cost for doing nothing! break; case AI_ACTION_CHANGE_FACING: // turn to face another direction bMinPointsNeeded = (INT8) GetAPsToLook( pSoldier ); break; case AI_ACTION_RANDOM_PATROL: // move towards a particular location case AI_ACTION_SEEK_FRIEND: // move towards friend in trouble case AI_ACTION_SEEK_OPPONENT: // move towards a reported opponent case AI_ACTION_TAKE_COVER: // run for nearest cover from threat case AI_ACTION_GET_CLOSER: // move closer to a strategic location case AI_ACTION_POINT_PATROL: // move towards next patrol point case AI_ACTION_LEAVE_WATER_GAS: // seek nearest spot of ungassed land case AI_ACTION_SEEK_NOISE: // seek most important noise heard case AI_ACTION_ESCORTED_MOVE: // go where told to by escortPlayer case AI_ACTION_RUN_AWAY: // run away from nearby opponent(s) case AI_ACTION_APPROACH_MERC: case AI_ACTION_TRACK: case AI_ACTION_EAT: case AI_ACTION_SCHEDULE_MOVE: case AI_ACTION_WALK: case AI_ACTION_MOVE_TO_CLIMB: // for movement, must have enough APs to move at least 1 tile's worth bMinPointsNeeded = MinPtsToMove(pSoldier); break; case AI_ACTION_PICKUP_ITEM: // grab things lying on the ground bMinPointsNeeded = __max( MinPtsToMove( pSoldier ), GetBasicAPsToPickupItem( pSoldier ) ); // SANDRO break; case AI_ACTION_OPEN_OR_CLOSE_DOOR: case AI_ACTION_UNLOCK_DOOR: case AI_ACTION_LOCK_DOOR: bMinPointsNeeded = MinPtsToMove(pSoldier); break; case AI_ACTION_DROP_ITEM: bMinPointsNeeded = GetBasicAPsToPickupItem( pSoldier ); // SANDRO break; case AI_ACTION_FIRE_GUN: // shoot at nearby opponent case AI_ACTION_TOSS_PROJECTILE: // throw grenade at/near opponent(s) case AI_ACTION_KNIFE_MOVE: // preparing to stab adjacent opponent case AI_ACTION_THROW_KNIFE: // only FIRE_GUN currently actually pays extra turning costs! bMinPointsNeeded = MinAPsToAttack(pSoldier,pSoldier->aiData.usActionData,ADDTURNCOST,pSoldier->aiData.bAimTime); #ifdef BETAVERSION if (ptsNeeded > pSoldier->bActionPoints) { /* sprintf(tempstr,"AI ERROR: %s has insufficient points for attack action %d at grid %d", pSoldier->name,pSoldier->aiData.bAction,pSoldier->aiData.usActionData); PopMessage(tempstr); */ } #endif break; case AI_ACTION_PULL_TRIGGER: // activate an adjacent panic trigger bMinPointsNeeded = APBPConstants[AP_PULL_TRIGGER]; break; case AI_ACTION_USE_DETONATOR: // grab detonator and set off bomb(s) bMinPointsNeeded = APBPConstants[AP_USE_REMOTE]; break; case AI_ACTION_YELLOW_ALERT: // tell friends opponent(s) heard case AI_ACTION_RED_ALERT: // tell friends opponent(s) seen case AI_ACTION_CREATURE_CALL: // for now bMinPointsNeeded = APBPConstants[AP_RADIO]; break; case AI_ACTION_CHANGE_STANCE: // crouch bMinPointsNeeded = GetAPsCrouch(pSoldier, TRUE); break; case AI_ACTION_GIVE_AID: // help injured/dying friend bMinPointsNeeded = 0; break; case AI_ACTION_CLIMB_ROOF: { INT8 bAPForStandUp = 0; INT8 bAPToLookAtWall = (FindDirectionForClimbing( pSoldier, pSoldier->sGridNo, pSoldier->pathing.bLevel ) == pSoldier->ubDirection) ? 0 : GetAPsToLook( pSoldier ); // SANDRO - improved this a bit if (pSoldier->pathing.bLevel == 0) { if( PTR_CROUCHED ) bAPForStandUp = (INT8)(GetAPsCrouch(pSoldier, TRUE)); else if( PTR_PRONE ) bAPForStandUp = GetAPsCrouch(pSoldier, TRUE) + GetAPsProne(pSoldier, TRUE); bMinPointsNeeded = GetAPsToClimbRoof( pSoldier, FALSE ) + bAPForStandUp + bAPToLookAtWall; } else { if( !PTR_CROUCHED ) bAPForStandUp = (INT8)(GetAPsCrouch(pSoldier, TRUE)); bMinPointsNeeded = GetAPsToClimbRoof( pSoldier, TRUE ) + bAPForStandUp + bAPToLookAtWall; } } break; case AI_ACTION_COWER: case AI_ACTION_STOP_COWERING: case AI_ACTION_LOWER_GUN: case AI_ACTION_END_COWER_AND_MOVE: case AI_ACTION_TRAVERSE_DOWN: case AI_ACTION_OFFER_SURRENDER: bMinPointsNeeded = 0; break; case AI_ACTION_STEAL_MOVE: // added by SANDRO //bMinPointsNeeded = GetAPsToStealItem( pSoldier, NULL, pSoldier->aiData.usActionData );; break; case AI_ACTION_JUMP_WINDOW: if (!pSoldier->CanClimbWithCurrentBackpack()) bMinPointsNeeded = GetAPsToJumpThroughWindows( pSoldier, TRUE ); else bMinPointsNeeded = GetAPsToJumpFence( pSoldier, FALSE ); break; case AI_ACTION_FREE_PRISONER: bMinPointsNeeded = APBPConstants[AP_HANDCUFF]; break; case AI_ACTION_USE_SKILL: bMinPointsNeeded = 10; // TODO break; case AI_ACTION_DOCTOR: case AI_ACTION_DOCTOR_SELF: bMinPointsNeeded = 20; // TODO break; case AI_ACTION_SELFDETONATE: bMinPointsNeeded = 20; // TODO break; default: #ifdef BETAVERSION //NumMessage("AffordableAction - Illegal action type = ",pSoldier->aiData.bAction); #endif break; } // check whether or not we can afford to do this action if (bMinPointsNeeded > pSoldier->bActionPoints) { return(FALSE); } else { return(TRUE); } } INT16 RandomFriendWithin(SOLDIERTYPE *pSoldier) { UINT32 uiLoop; UINT16 usMaxDist; UINT16 ubFriendCount; SoldierID ubFriendIDs[MAXMERCS], ubFriendID; UINT8 ubDirection; UINT8 ubDirsLeft; BOOLEAN fDirChecked[8]; BOOLEAN fFound = FALSE; INT32 usDest, usOrigin; SOLDIERTYPE * pFriend; // obtain maximum roaming distance from soldier's origin usMaxDist = RoamingRange(pSoldier,&usOrigin); // if range is restricted, make sure origin is a legal gridno! if (usOrigin < 0 || usOrigin >= GRIDSIZE) { #ifdef BETAVERSION NameMessage(pSoldier,"has illegal origin, but his roaming range is restricted!",1000); #endif return(FALSE); } ubFriendCount = 0; // build a list of the guynums of all active, eligible friendly mercs // go through each soldier, looking for "friends" (soldiers on same side) for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { pFriend = MercSlots[ uiLoop ]; // if this merc is inactive, not in sector, or dead if (!pFriend) { continue; } // skip ourselves if (pFriend->ubID == pSoldier->ubID) { continue; } // if this man not neutral, but is on my side, OR if he is neutral, but // so am I, then he's a "friend" for the purposes of random visitations if ((!pFriend->aiData.bNeutral && (pSoldier->bSide == pFriend->bSide)) || (pFriend->aiData.bNeutral && pSoldier->aiData.bNeutral)) { // if we're not already neighbors if (SpacesAway(pSoldier->sGridNo,pFriend->sGridNo) > 1) { // remember his guynum, increment friend counter ubFriendIDs[ubFriendCount++] = pFriend->ubID; } } } while (ubFriendCount && !fFound) { // randomly select one of the remaining friends in the list ubFriendID = ubFriendIDs[PreRandom(ubFriendCount)]; // if our movement range is NOT restricted, or this friend's within range // use distance - 1, because there must be at least 1 tile 1 space closer if (SpacesAway(usOrigin,Menptr[ubFriendID].sGridNo) - 1 <= usMaxDist) { // should be close enough, try to find a legal->pathing.sDestination within 1 tile // clear dirChecked flag for all 8 directions for ( ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ++ubDirection ) { fDirChecked[ubDirection] = FALSE; } ubDirsLeft = NUM_WORLD_DIRECTIONS; // examine all 8 spots around 'ubFriendID' // keep looking while directions remain and a satisfactory one not found while ((ubDirsLeft--) && !fFound) { // randomly select a direction which hasn't been 'checked' yet do { ubDirection = (UINT8)Random( NUM_WORLD_DIRECTIONS ); } while (fDirChecked[ubDirection]); fDirChecked[ubDirection] = TRUE; // determine the gridno 1 tile away from current friend in this direction usDest = NewGridNo(Menptr[ubFriendID].sGridNo,DirectionInc(ubDirection)); // if that's out of bounds, ignore it & check next direction if (usDest == Menptr[ubFriendID].sGridNo) { continue; } if (SpacesAway(usOrigin, usDest) > usMaxDist) { continue; } if (!CheckNPCDestination(pSoldier, usDest)) { continue; } if (LegalNPCDestination(pSoldier, usDest, ENSURE_PATH, NOWATER, 0)) { fFound = TRUE; // found a spot pSoldier->aiData.usActionData = usDest; // store this->pathing.sDestination pSoldier->pathing.bPathStored = TRUE; // optimization - Ian break; // stop checking in other directions } } } if (!fFound) { --ubFriendCount; // if we hadn't already picked the last friend currently in the list if (ubFriendCount != ubFriendID) { ubFriendIDs[ubFriendID] = ubFriendIDs[ubFriendCount]; } } } return(fFound); } INT32 RandDestWithinRange(SOLDIERTYPE *pSoldier) { INT32 sRandDest = NOWHERE; INT32 usOrigin, usMaxDist; UINT8 ubTriesLeft; BOOLEAN fLimited = FALSE, fFound = FALSE; INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXRange, sYRange, sXOffset, sYOffset; INT16 sOrigX, sOrigY; INT16 sX, sY; //DBrot: More Rooms //UINT8 ubRoom = 0, ubTempRoom; UINT16 usRoom = 0, usTempRoom; sOrigX = sOrigY = -1; sMaxLeft = sMaxRight = sMaxUp = sMaxDown = sXRange = sYRange = -1; // Try to find a random->pathing.sDestination that's no more than maxDist away from // the given gridno of origin if (gfTurnBasedAI) { ubTriesLeft = 10; } else { ubTriesLeft = 1; } usMaxDist = RoamingRange(pSoldier,&usOrigin); if ( pSoldier->aiData.bOrders <= CLOSEPATROL && (pSoldier->bTeam == CIV_TEAM || pSoldier->ubProfile != NO_PROFILE ) ) { // any other combo uses the default of ubRoom == 0, set above if ( !InARoom( pSoldier->aiData.sPatrolGrid[0], &usRoom ) ) { usRoom = 0; } } // if the maxDist is truly a restriction if (usMaxDist < (MAXCOL - 1)) { fLimited = TRUE; // determine maximum horizontal limits sOrigX = usOrigin % MAXCOL; sOrigY = usOrigin / MAXCOL; sMaxLeft = min(usMaxDist, sOrigX); sMaxRight = min(usMaxDist,MAXCOL - (sOrigX + 1)); // determine maximum vertical limits sMaxUp = min(usMaxDist, sOrigY); sMaxDown = min(usMaxDist,MAXROW - (sOrigY + 1)); sXRange = sMaxLeft + sMaxRight + 1; sYRange = sMaxUp + sMaxDown + 1; } if (pSoldier->ubBodyType == LARVAE_MONSTER) { // only crawl 1 tile, within our roaming range while ((ubTriesLeft--) && !fFound) { sXOffset = (INT16) Random( 3 ) - 1; // generates -1 to +1 sYOffset = (INT16) Random( 3 ) - 1; if (fLimited) { sX = pSoldier->sGridNo % MAXCOL + sXOffset; sY = pSoldier->sGridNo / MAXCOL + sYOffset; if (sX < sOrigX - sMaxLeft || sX > sOrigX + sMaxRight) { continue; } if (sY < sOrigY - sMaxUp || sY > sOrigY + sMaxDown) { continue; } sRandDest = usOrigin + sXOffset + (MAXCOL * sYOffset); } else { sRandDest = usOrigin + sXOffset + (MAXCOL * sYOffset); } if (!LegalNPCDestination(pSoldier,sRandDest,ENSURE_PATH,NOWATER,0)) { sRandDest = NOWHERE; continue; // try again! } // passed all the tests,->pathing.sDestination is acceptable fFound = TRUE; pSoldier->pathing.bPathStored = TRUE; // optimization - Ian } } else { // keep rolling random->pathing.sDestinations until one's satisfactory or retries used while ((ubTriesLeft--) && !fFound) { if (fLimited) { UINT8 ubTriesLeft2 = 128; do//dnl ch53 111009 This loop should increase search performance, but probably need some counter to prevent eventual endless loop { sXOffset = ((INT16)Random(sXRange)) - sMaxLeft; sYOffset = ((INT16)Random(sYRange)) - sMaxUp; sRandDest = usOrigin + sXOffset + (MAXCOL * sYOffset); }while(!GridNoOnVisibleWorldTile(sRandDest) && --ubTriesLeft2); #ifdef BETAVERSION if ((sRandDest < 0) || (sRandDest >= GRIDSIZE)) { NumMessage("RandDestWithinRange: ERROR - Gridno out of range! = ",sRandDest); sRandDest = random(GRIDSIZE); } #endif } else { UINT8 ubTriesLeft2 = 128; do//dnl ch53 111009 This loop should increase search performance, but probably need some counter to prevent eventual endless loop { sRandDest = PreRandom(GRIDSIZE); }while(!GridNoOnVisibleWorldTile(sRandDest) && --ubTriesLeft2); } if ( usRoom && InARoom( sRandDest, &usTempRoom ) && usTempRoom != usRoom ) { // outside of room available for patrol! sRandDest = NOWHERE; continue; } if (!CheckNPCDestination(pSoldier, sRandDest)) { sRandDest = NOWHERE; continue; } if (!LegalNPCDestination(pSoldier,sRandDest,ENSURE_PATH,NOWATER,0)) { sRandDest = NOWHERE; continue; // try again! } // passed all the tests,->pathing.sDestination is acceptable fFound = TRUE; pSoldier->pathing.bPathStored = TRUE; // optimization - Ian } } return(sRandDest); // defaults to NOWHERE } INT32 ClosestReachableDisturbance(SOLDIERTYPE *pSoldier, BOOLEAN * pfChangeLevel ) { INT32 *psLastLoc, *pusNoiseGridNo; INT8 *pbLastLevel; INT32 sGridNo=-1; INT8 bLevel, bClosestLevel = -1; BOOLEAN fClimbingNecessary, fClosestClimbingNecessary = FALSE; INT32 iPathCost; INT32 sClosestDisturbance = NOWHERE; UINT32 uiLoop; UINT32 closestConscious = NOWHERE,closestUnconscious = NOWHERE; INT32 iShortestPath = 1000; INT32 iShortestPathConscious = 1000,iShortestPathUnconscious = 1000; UINT8 *pubNoiseVolume; INT8 *pbNoiseLevel; INT8 *pbPersOL,*pbPublOL; INT32 sClimbGridNo; SOLDIERTYPE *pOpponent; SOLDIERTYPE *pClosestOpponent = NULL; INT32 sDistToEnemy, sDistToClosestEnemy = 10000; // sevenfm: safety check if ( pfChangeLevel ) { *pfChangeLevel = FALSE; } pubNoiseVolume = &gubPublicNoiseVolume[pSoldier->bTeam]; pusNoiseGridNo = &gsPublicNoiseGridNo[pSoldier->bTeam]; pbNoiseLevel = &gbPublicNoiseLevel[pSoldier->bTeam]; // hang pointers at start of this guy's personal and public opponent opplists // pbPersOL = &pSoldier->aiData.bOppList[0]; // pbPublOL = &(gbPublicOpplist[pSoldier->bTeam][0]); // psLastLoc = &(gsLastKnownOppLoc[pSoldier->ubID][0]); // look through this man's personal & public opplists for opponents known for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { pOpponent = MercSlots[ uiLoop ]; // if this merc is inactive, at base, on assignment, or dead if (!pOpponent) { continue; // next merc } // if this merc is neutral/on same side, he's not an opponent if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide) ) { continue; // next merc } // silversurfer: ignore empty vehicles if ( pOpponent->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle( pOpponent->bVehicleID ) == 0 ) { continue; } pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID; pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID; psLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpponent->ubID; pbLastLevel = gbLastKnownOppLevel[pSoldier->ubID] + pOpponent->ubID; // if this opponent is unknown personally and publicly if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN)) { continue; // next merc } // this is possible if get here from BLACK AI in one of those rare // instances when we couldn't get a meaningful shot off at a guy in sight /*if ((*pbPersOL == SEEN_CURRENTLY) && (pOpponent->stats.bLife >= OKLIFE)) { // don't allow this to return any valid values, this guy remains a // serious threat and the last thing we want to do is approach him! return(NOWHERE); }*/ // if personal knowledge is more up to date or at least equal if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) || (*pbPersOL == *pbPublOL)) { // using personal knowledge, obtain opponent's "best guess" gridno sGridNo = *psLastLoc; bLevel = *pbLastLevel; } else { // using public knowledge, obtain opponent's "best guess" gridno sGridNo = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID]; bLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpponent->ubID]; } // if we are standing at that gridno (!, obviously our info is old...) if (sGridNo == pSoldier->sGridNo) { continue; // next merc } if (TileIsOutOfBounds(sGridNo)) { // huh? continue; } // sevenfm: if soldier is zombie and he cannot climb, skip location if (pSoldier->IsZombie() && pSoldier->pathing.bLevel != bLevel && !gGameExternalOptions.fZombieCanClimb) { continue; } // sevenfm: zombies do not attack vehicles (rftr: or robots) if (pSoldier->IsZombie() && (AM_A_ROBOT(pOpponent) || ENEMYROBOT(pOpponent) || ARMED_VEHICLE(pOpponent) || (pOpponent->flags.uiStatusFlags & SOLDIER_VEHICLE))) { continue; } // sevenfm: when in deep water, skip opponents in deep water if (DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel) && DeepWater(sGridNo, bLevel)) { continue; } // sevenfm: if we found reachable enemy, check other enemies only if they are closer sDistToEnemy = PythSpacesAway( pSoldier->sGridNo, sGridNo ); if (sDistToEnemy < sDistToClosestEnemy || TileIsOutOfBounds(sClosestDisturbance) ) { iPathCost = EstimatePathCostToLocation( pSoldier, sGridNo, bLevel, FALSE, &fClimbingNecessary, &sClimbGridNo ); // if we can get there and it's first reachable enemy or closer than other known enemies if (iPathCost != 0 && (!pClosestOpponent || pClosestOpponent->stats.bLife < OKLIFE || pOpponent->stats.bLife >= OKLIFE) && (TileIsOutOfBounds(sClosestDisturbance) || iPathCost < iShortestPath || pClosestOpponent && !pClosestOpponent->IsZombie() && pClosestOpponent->stats.bLife < OKLIFE && pOpponent->stats.bLife >= OKLIFE)) { if (fClimbingNecessary) { sClosestDisturbance = sClimbGridNo; } else { sClosestDisturbance = sGridNo; } pClosestOpponent = pOpponent; sDistToClosestEnemy = sDistToEnemy; iShortestPath = iPathCost; fClosestClimbingNecessary = fClimbingNecessary; } } } // if any "misc. noise" was also heard recently if (!TileIsOutOfBounds(pSoldier->aiData.sNoiseGridno) && pSoldier->aiData.sNoiseGridno != sClosestDisturbance) { // test this gridno, too sGridNo = pSoldier->aiData.sNoiseGridno; bLevel = pSoldier->bNoiseLevel; // if we are there (at the noise gridno) if (sGridNo == pSoldier->sGridNo) { for(uiLoop=0; uiLoopbSide == pOpponent->bSide && pSoldier->ubID != pOpponent->ubID&& pSoldier->aiData.sNoiseGridno == pOpponent->aiData.sNoiseGridno) { pOpponent->aiData.sNoiseGridno = NOWHERE;// Erase for all from the same team as it not useful anymore, this will avoid others to check already tested location pOpponent->aiData.ubNoiseVolume = 0; } } pSoldier->aiData.sNoiseGridno = NOWHERE; // wipe it out, not useful anymore pSoldier->aiData.ubNoiseVolume = 0; } else { // get the AP cost to get to the location of the noise iPathCost = EstimatePathCostToLocation( pSoldier, sGridNo, bLevel, FALSE, &fClimbingNecessary, &sClimbGridNo ); // if we can get there // sevenfm: only if we don't know enemy location or noise source is close and we have not seen enemy recently if (iPathCost != 0 && !AICheckIsFlanking(pSoldier) && (TileIsOutOfBounds( sClosestDisturbance ) || iPathCost < iShortestPath && !GuySawEnemy( pSoldier )) ) { if (fClimbingNecessary) { sClosestDisturbance = sClimbGridNo; } else { sClosestDisturbance = sGridNo; } iShortestPath = iPathCost; fClosestClimbingNecessary = fClimbingNecessary; } } } // if any PUBLIC "misc. noise" was also heard recently if (!TileIsOutOfBounds(*pusNoiseGridNo) && *pusNoiseGridNo != sClosestDisturbance ) { // test this gridno, too sGridNo = *pusNoiseGridNo; bLevel = *pbNoiseLevel; // if we are not NEAR the noise gridno... if ( pSoldier->pathing.bLevel != bLevel || PythSpacesAway( pSoldier->sGridNo, sGridNo ) >= 6 || SoldierTo3DLocationLineOfSightTest( pSoldier, sGridNo, bLevel, 0, FALSE, NO_DISTANCE_LIMIT ) == 0 ) // if we are NOT there (at the noise gridno) // if (sGridNo != pSoldier->sGridNo) { // get the AP cost to get to the location of the noise iPathCost = EstimatePathCostToLocation( pSoldier, sGridNo, bLevel, FALSE, &fClimbingNecessary, &sClimbGridNo ); // if we can get there // sevenfm: only if we don't know enemy location or noise source is close and we have not seen enemy recently if (iPathCost != 0 && !AICheckIsFlanking(pSoldier) && (TileIsOutOfBounds(sClosestDisturbance) || iPathCost < iShortestPath && !GuySawEnemy(pSoldier))) { if (fClimbingNecessary) { sClosestDisturbance = sClimbGridNo; } else { sClosestDisturbance = sGridNo; } iShortestPath = iPathCost; fClosestClimbingNecessary = fClimbingNecessary; } } else { // degrade our public noise a bit //dnl ch58 160813 //*pusNoiseGridNo -= 2; if(*pubNoiseVolume > 1) (*pubNoiseVolume)--; } } #ifdef DEBUGDECISIONS if (!TileIsOutOfBounds(sClosestDisturbance)) { AINumMessage("CLOSEST DISTURBANCE is at gridno ",sClosestDisturbance); } #endif // sevenfm: safety check if ( pfChangeLevel ) { *pfChangeLevel = fClosestClimbingNecessary; } return(sClosestDisturbance); } INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel, SoldierID * pubOpponentID, INT32 * distanceInCellCoords) { INT32 *psLastLoc,sGridNo, sClosestOpponent = NOWHERE; UINT32 uiLoop; INT32 iRange, iClosestRange = 1500; INT8 *pbPersOL,*pbPublOL; INT8 bLevel, bClosestLevel; SOLDIERTYPE *pOpponent; SOLDIERTYPE *pClosestOpponent = NULL; bClosestLevel = -1; if (pubOpponentID) { *pubOpponentID = NOBODY; } // NOTE: THIS FUNCTION ALLOWS RETURN OF UNCONSCIOUS AND UNREACHABLE OPPONENTS psLastLoc = &(gsLastKnownOppLoc[pSoldier->ubID][0]); // hang pointers at start of this guy's personal and public opponent opplists pbPersOL = &pSoldier->aiData.bOppList[0]; pbPublOL = &(gbPublicOpplist[pSoldier->bTeam][0]); // look through this man's personal & public opplists for opponents known for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { pOpponent = MercSlots[ uiLoop ]; // if this merc is inactive, at base, on assignment, or dead if (!pOpponent) { continue; // next merc } // if this merc is neutral/on same side, he's not an opponent if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide)) { continue; // next merc } // silversurfer: ignore empty vehicles 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) { continue; // next opponent } pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID; pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID; psLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpponent->ubID; // if this opponent is unknown personally and publicly if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN)) { continue; // next merc } // if personal knowledge is more up to date or at least equal if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) || (*pbPersOL == *pbPublOL)) { // using personal knowledge, obtain opponent's "best guess" gridno sGridNo = gsLastKnownOppLoc[pSoldier->ubID][pOpponent->ubID]; bLevel = gbLastKnownOppLevel[pSoldier->ubID][pOpponent->ubID]; } else { // using public knowledge, obtain opponent's "best guess" gridno sGridNo = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID]; bLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpponent->ubID]; } // if we are standing at that gridno(!, obviously our info is old...) if (sGridNo == pSoldier->sGridNo) { continue; // next merc } // this function is used only for turning towards closest opponent or changing stance // as such, if they AI is in a building, // we should ignore people who are on the roof of the same building as the AI if ( (bLevel != pSoldier->pathing.bLevel) && SameBuilding( pSoldier->sGridNo, sGridNo ) ) { continue; } // I hope this will be good enough; otherwise we need a fractional/world-units-based 2D distance function //sRange = PythSpacesAway( pSoldier->sGridNo, sGridNo); iRange = GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, sGridNo ); if (sClosestOpponent == NOWHERE || iRange < iClosestRange || pClosestOpponent && !pClosestOpponent->IsZombie() && !BOXER(pSoldier) && pClosestOpponent->stats.bLife < OKLIFE && pOpponent->stats.bLife >= OKLIFE) { iClosestRange = iRange; sClosestOpponent = sGridNo; bClosestLevel = bLevel; pClosestOpponent = pOpponent; } } #ifdef DEBUGDECISIONS if (!TileIsOutOfBounds(sClosestOpponent)) { AINumMessage("CLOSEST OPPONENT is at gridno ",sClosestOpponent); } #endif if (psGridNo) { *psGridNo = sClosestOpponent; } if (pbLevel) { *pbLevel = bClosestLevel; } if (pubOpponentID && pClosestOpponent) { *pubOpponentID = pClosestOpponent->ubID; } if ( distanceInCellCoords ) { *distanceInCellCoords = iClosestRange; } return( sClosestOpponent ); } INT32 ClosestSeenOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel) { INT32 sGridNo, sClosestOpponent = NOWHERE; UINT32 uiLoop; INT32 iRange, iClosestRange = 1500; INT8 *pbPersOL; INT8 bLevel, bClosestLevel; SOLDIERTYPE * pOpp; bClosestLevel = -1; // look through this man's personal & public opplists for opponents known for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { pOpp = MercSlots[ uiLoop ]; // if this merc is inactive, at base, on assignment, or dead if (!pOpp) { continue; // next merc } // if this merc is neutral/on same side, he's not an opponent if ( CONSIDERED_NEUTRAL( pSoldier, pOpp ) || (pSoldier->bSide == pOpp->bSide)) { continue; // next merc } // silversurfer: ignore empty vehicles if ( pOpp->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle( pOpp->bVehicleID ) == 0 ) continue; // Special stuff for Carmen the bounty hunter if (pSoldier->aiData.bAttitude == ATTACKSLAYONLY && pOpp->ubProfile != SLAY) { continue; // next opponent } pbPersOL = pSoldier->aiData.bOppList + pOpp->ubID; // if this opponent is not seen personally if (*pbPersOL != SEEN_CURRENTLY) { continue; // next merc } // since we're dealing with seen people, use exact gridnos sGridNo = pOpp->sGridNo; bLevel = pOpp->pathing.bLevel; // if we are standing at that gridno(!, obviously our info is old...) if (sGridNo == pSoldier->sGridNo) { continue; // next merc } // this function is used only for turning towards closest opponent or changing stance // as such, if they AI is in a building, // we should ignore people who are on the roof of the same building as the AI if ( (bLevel != pSoldier->pathing.bLevel) && SameBuilding( pSoldier->sGridNo, sGridNo ) ) { continue; } // I hope this will be good enough; otherwise we need a fractional/world-units-based 2D distance function //sRange = PythSpacesAway( pSoldier->sGridNo, sGridNo); iRange = GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, sGridNo ); if (iRange < iClosestRange) { iClosestRange = iRange; sClosestOpponent = sGridNo; bClosestLevel = bLevel; } } #ifdef DEBUGDECISIONS if (!TileIsOutOfBounds(sClosestOpponent)) { AINumMessage("CLOSEST OPPONENT is at gridno ",sClosestOpponent); } #endif if (psGridNo) { *psGridNo = sClosestOpponent; } if (pbLevel) { *pbLevel = bClosestLevel; } return( sClosestOpponent ); } // special variant with a minor twist INT32 ClosestSeenOpponentWithRoof(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel) { INT32 sGridNo, sClosestOpponent = NOWHERE; UINT32 uiLoop; INT32 iRange, iClosestRange = 1500; INT8 *pbPersOL; INT8 bLevel, bClosestLevel; SOLDIERTYPE * pOpp; bClosestLevel = -1; // look through this man's personal & public opplists for opponents known for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { pOpp = MercSlots[ uiLoop ]; // if this merc is inactive, at base, on assignment, or dead if (!pOpp) { continue; // next merc } // if this merc is neutral/on same side, he's not an opponent if ( CONSIDERED_NEUTRAL( pSoldier, pOpp ) || (pSoldier->bSide == pOpp->bSide)) { continue; // next merc } // silversurfer: ignore empty vehicles if ( pOpp->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle( pOpp->bVehicleID ) == 0 ) continue; // Special stuff for Carmen the bounty hunter if (pSoldier->aiData.bAttitude == ATTACKSLAYONLY && pOpp->ubProfile != SLAY) { continue; // next opponent } pbPersOL = pSoldier->aiData.bOppList + pOpp->ubID; // if this opponent is not seen personally if (*pbPersOL != SEEN_CURRENTLY) { continue; // next merc } // since we're dealing with seen people, use exact gridnos sGridNo = pOpp->sGridNo; bLevel = pOpp->pathing.bLevel; // if we are standing at that gridno(!, obviously our info is old...) if (sGridNo == pSoldier->sGridNo) { continue; // next merc } // special: allow zombies to also find opponents on a roof // otherwise they have will never decide to climb a roof while they are seeing an enemy // this function is used only for turning towards closest opponent or changing stance // as such, if they AI is in a building, // we should ignore people who are on the roof of the same building as the AI /*if ( !pSoldier->IsZombie() && (bLevel != pSoldier->pathing.bLevel) && SameBuilding( pSoldier->sGridNo, sGridNo ) ) { continue; }*/ // I hope this will be good enough; otherwise we need a fractional/world-units-based 2D distance function //sRange = PythSpacesAway( pSoldier->sGridNo, sGridNo); iRange = GetRangeInCellCoordsFromGridNoDiff( pSoldier->sGridNo, sGridNo ); if (iRange < iClosestRange) { iClosestRange = iRange; sClosestOpponent = sGridNo; bClosestLevel = bLevel; } } #ifdef DEBUGDECISIONS if (!TileIsOutOfBounds(sClosestOpponent)) { AINumMessage("CLOSEST OPPONENT is at gridno ",sClosestOpponent); } #endif if (psGridNo) { *psGridNo = sClosestOpponent; } if (pbLevel) { *pbLevel = bClosestLevel; } return( sClosestOpponent ); } INT32 ClosestPC( SOLDIERTYPE *pSoldier, INT32 * psDistance ) { // used by NPCs... find the closest PC // NOTE: skips EPCs! SOLDIERTYPE *pTargetSoldier; INT32 sMinDist = WORLD_MAX; INT32 sDist; INT32 sGridNo = NOWHERE; // Loop through all mercs on player team SoldierID ubLoop = gTacticalStatus.Team[ gbPlayerNum ].bFirstID; for ( ; ubLoop <= gTacticalStatus.Team[ gbPlayerNum ].bLastID; ++ubLoop) { pTargetSoldier = ubLoop; if (!pTargetSoldier->bActive || !pTargetSoldier->bInSector) { continue; } // if not conscious, skip him if (pTargetSoldier->stats.bLife < OKLIFE) { continue; } if ( AM_AN_EPC( pTargetSoldier ) ) { continue; } sDist = PythSpacesAway(pSoldier->sGridNo,pTargetSoldier->sGridNo); // if this PC is not visible to the soldier, then add a penalty to the distance // so that we weight in favour of visible mercs if ( pTargetSoldier->bTeam != pSoldier->bTeam && pSoldier->aiData.bOppList[ ubLoop ] != SEEN_CURRENTLY ) { sDist += 10; } if (sDist < sMinDist) { sMinDist = sDist; sGridNo = pTargetSoldier->sGridNo; } } if ( psDistance ) { *psDistance = sMinDist; } return( sGridNo ); } // Flugente: like ClosestPC(...), but does not account for covert or not visible mercs INT32 ClosestUnDisguisedPC( SOLDIERTYPE *pSoldier, INT32 * psDistance ) { // used by NPCs... find the closest PC // NOTE: skips EPCs! SOLDIERTYPE *pTargetSoldier; INT32 sMinDist = WORLD_MAX; INT32 sDist; INT32 sGridNo = NOWHERE; // Loop through all mercs on player team SoldierID ubLoop = gTacticalStatus.Team[ gbPlayerNum ].bFirstID; for ( ; ubLoop <= gTacticalStatus.Team[ gbPlayerNum ].bLastID; ++ubLoop ) { pTargetSoldier = ubLoop; if (!pTargetSoldier->bActive || !pTargetSoldier->bInSector) continue; // if not conscious, skip him if (pTargetSoldier->stats.bLife < OKLIFE) continue; if ( AM_AN_EPC( pTargetSoldier ) ) continue; if ( pTargetSoldier->usSoldierFlagMask & (SOLDIER_COVERT_CIV|SOLDIER_COVERT_SOLDIER) ) continue; sDist = PythSpacesAway(pSoldier->sGridNo,pTargetSoldier->sGridNo); // if this PC is not visible to the soldier, then add a penalty to the distance // so that we weight in favour of visible mercs if ( pTargetSoldier->bTeam != pSoldier->bTeam && pSoldier->aiData.bOppList[ ubLoop ] != SEEN_CURRENTLY ) continue; if (sDist < sMinDist) { sMinDist = sDist; sGridNo = pTargetSoldier->sGridNo; } } if ( psDistance ) { *psDistance = sMinDist; } return( sGridNo ); } INT32 FindClosestClimbPointAvailableToAI( SOLDIERTYPE * pSoldier, INT32 sStartGridNo, INT32 sDesiredGridNo, BOOLEAN fClimbUp ) { // sevenfm: safety check if (!pSoldier) { return NOWHERE; } // sevenfm: don't check roaming range return FindClosestClimbPoint(pSoldier, sStartGridNo, sDesiredGridNo, fClimbUp); /*INT32 sGridNo; INT32 sRoamingOrigin; INT16 sRoamingRange; if ( pSoldier->flags.uiStatusFlags & SOLDIER_PC ) { sRoamingOrigin = pSoldier->sGridNo; sRoamingRange = 99; } else { sRoamingRange = RoamingRange( pSoldier, &sRoamingOrigin ); } // since climbing necessary involves going an extra tile, we compare against 1 less than the roam range... // or add 1 to the distance to the climb point sGridNo = FindClosestClimbPoint( pSoldier, sStartGridNo, sDesiredGridNo, fClimbUp ); if ( PythSpacesAway( sRoamingOrigin, sGridNo ) + 1 > sRoamingRange ) { return( NOWHERE ); } else { return( sGridNo ); }*/ } BOOLEAN ClimbingNecessary( SOLDIERTYPE * pSoldier, INT32 sDestGridNo, INT8 bDestLevel ) { if (pSoldier->pathing.bLevel == bDestLevel) { if ( (pSoldier->pathing.bLevel == 0) || ( gubBuildingInfo[ pSoldier->sGridNo ] == gubBuildingInfo[ sDestGridNo ] ) ) { return( FALSE ); } else // different buildings! { return( TRUE ); } } else { return( TRUE ); } } INT32 GetInterveningClimbingLocation( SOLDIERTYPE * pSoldier, INT32 sDestGridNo, INT8 bDestLevel, BOOLEAN * pfClimbingNecessary ) { if (pSoldier->pathing.bLevel == bDestLevel) { if ( (pSoldier->pathing.bLevel == 0) || ( gubBuildingInfo[ pSoldier->sGridNo ] == gubBuildingInfo[ sDestGridNo ] ) ) { // on ground or same building... normal! *pfClimbingNecessary = FALSE; return( NOWHERE ); } else { // different buildings! // yes, pass in same gridno twice... want closest climb-down spot for building we are on! *pfClimbingNecessary = TRUE; return( FindClosestClimbPointAvailableToAI( pSoldier, pSoldier->sGridNo, pSoldier->sGridNo, FALSE ) ); } } else { *pfClimbingNecessary = TRUE; // different levels if (pSoldier->pathing.bLevel == 0) { // got to go UP onto building return( FindClosestClimbPointAvailableToAI( pSoldier, pSoldier->sGridNo, sDestGridNo, TRUE ) ); } else { // got to go DOWN off building return( FindClosestClimbPointAvailableToAI( pSoldier, pSoldier->sGridNo, pSoldier->sGridNo, FALSE ) ); } } } INT16 EstimatePathCostToLocation( SOLDIERTYPE * pSoldier, INT32 sDestGridNo, INT8 bDestLevel, BOOLEAN fAddCostAfterClimbingUp, BOOLEAN * pfClimbingNecessary, INT32 * psClimbGridNo ) { INT16 sPathCost; INT32 sClimbGridNo; if (pSoldier->pathing.bLevel == bDestLevel) { if ( (pSoldier->pathing.bLevel == 0) || ( gubBuildingInfo[ pSoldier->sGridNo ] == gubBuildingInfo[ sDestGridNo ] ) ) { // on ground or same building... normal! sPathCost = EstimatePlotPath( pSoldier, sDestGridNo, FALSE, FALSE, FALSE, WALKING, FALSE, FALSE, 0); if (pfClimbingNecessary) *pfClimbingNecessary = FALSE; if (psClimbGridNo) *psClimbGridNo = NOWHERE; } else { // different buildings! // yes, pass in same gridno twice... want closest climb-down spot for building we are on! sClimbGridNo = FindClosestClimbPointAvailableToAI( pSoldier, sDestGridNo, pSoldier->sGridNo, FALSE ); if (TileIsOutOfBounds(sClimbGridNo)) { sPathCost = 0; } else { sPathCost = PlotPath( pSoldier, sClimbGridNo, FALSE, FALSE, FALSE, WALKING, FALSE, FALSE, 0 ); // sevenfm: check if we are already standing at climb gridno if (sPathCost != 0 || pSoldier->sGridNo == sClimbGridNo) { // add in cost of climbing down if (fAddCostAfterClimbingUp) { // add in cost of later climbing up, too sPathCost += APBPConstants[AP_CLIMBOFFROOF] + APBPConstants[AP_CLIMBROOF]; // add in an estimate of getting there after climbing down sPathCost += (APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_WALK]) * PythSpacesAway( sClimbGridNo, sDestGridNo ); } else { sPathCost += APBPConstants[AP_CLIMBOFFROOF]; // add in an estimate of getting there after climbing down, *but not on top of roof* sPathCost += (APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_WALK]) * PythSpacesAway( sClimbGridNo, sDestGridNo ) / 2; } if (pfClimbingNecessary) *pfClimbingNecessary = TRUE; if (psClimbGridNo) *psClimbGridNo = sClimbGridNo; } } } } else { // sevenfm: check if zombie cannot climb if (pSoldier->IsZombie() && !gGameExternalOptions.fZombieCanClimb) { return 0; } // different levels if (pSoldier->pathing.bLevel == 0) { //got to go UP onto building sClimbGridNo = FindClosestClimbPointAvailableToAI( pSoldier, pSoldier->sGridNo, sDestGridNo, TRUE ); } else { // got to go DOWN off building sClimbGridNo = FindClosestClimbPointAvailableToAI( pSoldier, sDestGridNo, pSoldier->sGridNo, FALSE ); } if (TileIsOutOfBounds(sClimbGridNo)) { sPathCost = 0; } else { sPathCost = PlotPath( pSoldier, sClimbGridNo, FALSE, FALSE, FALSE, WALKING, FALSE, FALSE, 0); // sevenfm: check if we are already standing at climb gridno if (sPathCost != 0 || pSoldier->sGridNo == sClimbGridNo) { // add in the cost of climbing up or down if (pSoldier->pathing.bLevel == 0) { // must climb up sPathCost += APBPConstants[AP_CLIMBROOF]; if (fAddCostAfterClimbingUp) { // add to path a rough estimate of how far to go from the climb gridno to the friend // estimate walk cost sPathCost += (APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_WALK]) * PythSpacesAway( sClimbGridNo, sDestGridNo ); } } else { // must climb down sPathCost += APBPConstants[AP_CLIMBOFFROOF]; // add to path a rough estimate of how far to go from the climb gridno to the friend // estimate walk cost sPathCost += (APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_WALK]) * PythSpacesAway( sClimbGridNo, sDestGridNo ); } if (pfClimbingNecessary) *pfClimbingNecessary = TRUE; if (psClimbGridNo) *psClimbGridNo = sClimbGridNo; } } } return( sPathCost ); } BOOLEAN GuySawEnemy( SOLDIERTYPE * pSoldier, UINT8 ubMax ) { UINT8 ubTeamLoop; SOLDIERTYPE *pOpponent; for ( ubTeamLoop = 0; ubTeamLoop < MAXTEAMS; ++ubTeamLoop ) { if(!gTacticalStatus.Team[ubTeamLoop].bTeamActive)//dnl ch58 070913 skip any inactive teams continue; if ( gTacticalStatus.Team[ ubTeamLoop ].bSide != pSoldier->bSide ) { // consider guys in this team, which isn't on our side for ( SoldierID ubIDLoop = gTacticalStatus.Team[ ubTeamLoop ].bFirstID; ubIDLoop <= gTacticalStatus.Team[ ubTeamLoop ].bLastID; ++ubIDLoop ) { pOpponent = ubIDLoop; // if this merc is inactive, at base, on assignment, or dead if (!pOpponent) { continue; } // if this merc is neutral/on same side, he's not an opponent if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide) ) { continue; } // sevenfm: ignore empty vehicles if( pOpponent->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle( pOpponent->bVehicleID ) == 0 ) { continue; } // if this guy SAW an enemy recently... if( pSoldier->aiData.bOppList[ ubIDLoop ] >= SEEN_CURRENTLY && pSoldier->aiData.bOppList[ ubIDLoop ] <= ubMax ) { return( TRUE ); } } } } return( FALSE ); } INT32 ClosestReachableFriendInTrouble(SOLDIERTYPE *pSoldier, BOOLEAN * pfClimbingNecessary) { UINT32 uiLoop; INT32 sPathCost, sClosestFriend = NOWHERE, sShortestPath = 1000, sClimbGridNo; BOOLEAN fClimbingNecessary, fClosestClimbingNecessary = FALSE; SOLDIERTYPE *pFriend; INT32 sClosestKnownOpponent; BOOLEAN fCallHelp; // civilians don't really have any "friends", so they don't bother with this if (PTR_CIVILIAN) { return(NOWHERE); } // consider every friend of this soldier (locations assumed to be known) for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { pFriend = MercSlots[ uiLoop ]; // if this merc is inactive, at base, on assignment, or dead if (!pFriend) { continue; // next merc } // if this merc is neutral or NOT on the same side, he's not a friend if (pFriend->aiData.bNeutral || (pSoldier->bSide != pFriend->bSide)) { continue; // next merc } // if this "friend" is actually US if (pFriend->ubID == pSoldier->ubID) { continue; // next merc } sClosestKnownOpponent = ClosestKnownOpponent(pFriend, NULL, NULL); // CJC: restrict "last one to radio" to only if that guy saw us this turn or last turn fCallHelp = FALSE; // if this friend is under fire or he called for help recently if (pFriend->aiData.bUnderFire || (pFriend->ubID == gTacticalStatus.Team[pFriend->bTeam].ubLastMercToRadio && GuySawEnemy(pFriend))) { fCallHelp = TRUE; } // zombies always call for help if they know enemy position if (pSoldier->IsZombie() && pFriend->IsZombie() && !TileIsOutOfBounds(sClosestKnownOpponent)) { fCallHelp = TRUE; } if (!fCallHelp) { continue; // next merc } // if we're already neighbors if (SpacesAway(pSoldier->sGridNo,pFriend->sGridNo) == 1) { continue; // next merc } // get the AP cost to go to this friend's gridno sPathCost = EstimatePathCostToLocation( pSoldier, pFriend->sGridNo, pFriend->pathing.bLevel, TRUE, &fClimbingNecessary, &sClimbGridNo ); // if we can get there if (sPathCost != 0) { //sprintf(tempstr,"Path cost to friend %s's location is %d",pFriend->name,pathCost); //PopMessage(tempstr); if (sPathCost < sShortestPath) { if (fClimbingNecessary) { sClosestFriend = sClimbGridNo; } else { sClosestFriend = pFriend->sGridNo; } sShortestPath = sPathCost; fClosestClimbingNecessary = fClimbingNecessary; } } } #ifdef DEBUGDECISIONS if (!TileIsOutOfBounds(sClosestFriend)) { AINumMessage("CLOSEST FRIEND is at gridno ",sClosestFriend); } #endif *pfClimbingNecessary = fClosestClimbingNecessary; return(sClosestFriend); } INT16 DistanceToClosestFriend( SOLDIERTYPE * pSoldier ) { // find the distance to the closest person on the same team SOLDIERTYPE *pTargetSoldier; INT16 sMinDist = 1000; INT16 sDist; // Loop through all mercs on player team SoldierID ubLoop = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID; for ( ; ubLoop <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID; ++ubLoop ) { if (ubLoop == pSoldier->ubID) { // same guy - continue! continue; } pTargetSoldier = ubLoop; if ( pSoldier->bActive && pSoldier->bInSector ) { if (!pTargetSoldier->bActive || !pTargetSoldier->bInSector) { continue; } // if not conscious, skip him else if (pTargetSoldier->stats.bLife < OKLIFE) { continue; } } else { // compare sector #s if ( (pSoldier->sSectorX != pTargetSoldier->sSectorX) || (pSoldier->sSectorY != pTargetSoldier->sSectorY) || (pSoldier->bSectorZ != pTargetSoldier->bSectorZ) ) { continue; } else if (pTargetSoldier->stats.bLife < OKLIFE) { continue; } else { // well there's someone who could be near return( 1 ); } } sDist = SpacesAway(pSoldier->sGridNo,pTargetSoldier->sGridNo); if (sDist < sMinDist) { sMinDist = sDist; } } return( sMinDist ); } BOOLEAN InWaterGasOrSmoke( SOLDIERTYPE *pSoldier, INT32 sGridNo ) { if (WaterTooDeepForAttacks( sGridNo, pSoldier->pathing.bLevel )) { return(TRUE); } // smoke if (gpWorldLevelData[sGridNo].ubExtFlags[ pSoldier->pathing.bLevel ] & MAPELEMENT_EXT_SMOKE) { return TRUE; } return InGas( pSoldier, 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; return InGas(pSoldier,sGridNo); } INT16 InWaterOrGas(SOLDIERTYPE *pSoldier, INT32 sGridNo) { if (WaterTooDeepForAttacks( sGridNo, pSoldier->pathing.bLevel )) { return(TRUE); } return (INT16)InGas( pSoldier, sGridNo ); } BOOLEAN InGasSpot(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bLevel) { CHECKF(pSoldier); if (TileIsOutOfBounds(sGridNo)) return FALSE; // tear gas if ((gpWorldLevelData[sGridNo].ubExtFlags[bLevel] & MAPELEMENT_EXT_TEARGAS) && !DoesSoldierWearGasMask(pSoldier)) { return(TRUE); } // fire/creature/mustard gas // sevenfm: avoid mustard gas even when wearing gas mask if (gpWorldLevelData[sGridNo].ubExtFlags[bLevel] & (MAPELEMENT_EXT_BURNABLEGAS | MAPELEMENT_EXT_CREATUREGAS | MAPELEMENT_EXT_MUSTARDGAS)) { return(TRUE); } return FALSE; } BOOLEAN InGas(SOLDIERTYPE *pSoldier, INT32 sGridNo) { CHECKF(pSoldier); 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; } //WarmSteel - One square away from gas is still considered in gas, because it could expand any moment. //Note: this only works for gas that expands with one tile, but hey it's better than nothing! int iNeighbourGridNo; for (int iDir = 0; iDir < NUM_WORLD_DIRECTIONS; ++iDir) { iNeighbourGridNo = sGridNo + DirectionInc(iDir); if (!TileIsOutOfBounds(iNeighbourGridNo) && InGasSpot(pSoldier, iNeighbourGridNo, pSoldier->pathing.bLevel)) { return TRUE; } } return(FALSE); } BOOLEAN WearGasMaskIfAvailable( SOLDIERTYPE * pSoldier ) { INT8 bSlot, bNewSlot; bSlot = FindGasMask( pSoldier ); if ( bSlot == NO_SLOT ) { return( FALSE ); } if ( bSlot == HEAD1POS || bSlot == HEAD2POS || bSlot == HELMETPOS ) { return( FALSE ); } if ( pSoldier->inv[ HEAD1POS ].exists() == false ) { bNewSlot = HEAD1POS; } else if ( pSoldier->inv[ HEAD2POS ].exists() == false ) { bNewSlot = HEAD2POS; } else { // screw it, going in position 1 anyhow bNewSlot = HEAD1POS; } RearrangePocket( pSoldier, bSlot, bNewSlot, TRUE ); if ( pSoldier->bTeam == gbPlayerNum ) { pSoldier->DeleteSoldierLight( ); pSoldier->PositionSoldierLight( ); } return( TRUE ); } BOOLEAN InLightAtNight( INT32 sGridNo, INT8 bLevel ) { UINT8 ubBackgroundLightLevel; // do not consider us to be "in light" if we're in an underground sector if ( gbWorldSectorZ > 0 ) { return( FALSE ); } ubBackgroundLightLevel = GetTimeOfDayAmbientLightLevel(); if ( ubBackgroundLightLevel < NORMAL_LIGHTLEVEL_DAY + 2 ) { // don't consider it nighttime, too close to daylight levels return( FALSE ); } // could've been placed here, ignore the light if ( InARoom( sGridNo, NULL ) ) { return( FALSE ); } // NB light levels are backwards, so a lower light level means the // spot in question is BRIGHTER if ( LightTrueLevel( sGridNo, bLevel ) < ubBackgroundLightLevel ) { return( TRUE ); } return( FALSE ); } INT8 CalcMorale(SOLDIERTYPE *pSoldier) { UINT32 uiLoop, uiLoop2; INT32 iOurTotalThreat = 0, iTheirTotalThreat = 0; INT16 sOppThreatValue, sFrndThreatValue, sMorale; INT32 iPercent; INT8 bMostRecentOpplistValue; INT8 bMoraleCategory; UINT8 *pSeenOpp; //,*friendOlPtr; INT8 *pbPersOL, *pbPublOL; SOLDIERTYPE *pOpponent, *pFriend; // sevenfm: use new calculation: if (gGameExternalOptions.fAINewMorale) { return CalcMoraleNew(pSoldier); } // if army guy has NO weapons left then panic! if (pSoldier->bTeam == ENEMY_TEAM) { if (FindAIUsableObjClass(pSoldier, IC_WEAPON) == NO_SLOT) { return(MORALE_HOPELESS); } } // hang pointers to my personal opplist, my team's public opplist, and my // list of previously seen opponents pSeenOpp = (UINT8 *)&(gbSeenOpponents[pSoldier->ubID][0]); // loop through every one of my possible opponents for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { pOpponent = MercSlots[uiLoop]; // if this merc is inactive, at base, on assignment, dead, unconscious if (!pOpponent || (pOpponent->stats.bLife < OKLIFE)) continue; // next merc // if this merc is neutral/on same side, he's not an opponent, skip him! if (CONSIDERED_NEUTRAL(pSoldier, pOpponent) || (pSoldier->bSide == pOpponent->bSide)) continue; // next merc // Special stuff for Carmen the bounty hunter if (pSoldier->aiData.bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != SLAY) { continue; // next opponent } pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID; pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID; pSeenOpp = (UINT8 *)gbSeenOpponents[pSoldier->ubID] + pOpponent->ubID; // if this opponent is unknown to me personally AND unknown to my team, too if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN)) { // if I have never seen him before anywhere in this sector, either if (!(*pSeenOpp)) continue; // next merc // have seen him in the past, so he remains something of a threat bMostRecentOpplistValue = 0; // uses the free slot for 0 opplist } else // decide which opplist is more current { // if personal knowledge is more up to date or at least equal if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) || (*pbPersOL == *pbPublOL)) bMostRecentOpplistValue = *pbPersOL; // use personal else bMostRecentOpplistValue = *pbPublOL; // use public } iPercent = ThreatPercent[bMostRecentOpplistValue - OLDEST_HEARD_VALUE]; sOppThreatValue = (iPercent * CalcManThreatValue(pOpponent, pSoldier->sGridNo, FALSE, pSoldier)) / 100; //sprintf(tempstr,"Known opponent %s, opplist status %d, percent %d, threat = %d", // ExtMen[pOpponent->ubID].name,ubMostRecentOpplistValue,ubPercent,sOppThreatValue); //PopMessage(tempstr); // ADD this to their running total threatValue (decreases my MORALE) iTheirTotalThreat += sOppThreatValue; //NumMessage("Their TOTAL threat now = ",sTheirTotalThreat); // NOW THE FUN PART: SINCE THIS OPPONENT IS KNOWN TO ME IN SOME WAY, // ANY FRIENDS OF MINE THAT KNOW ABOUT HIM BOOST MY MORALE. SO, LET'S GO // THROUGH THEIR PERSONAL OPPLISTS AND CHECK WHICH OF MY FRIENDS KNOW // SOMETHING ABOUT HIM AND WHAT THEIR THREAT VALUE TO HIM IS. for (uiLoop2 = 0; uiLoop2 < guiNumMercSlots; uiLoop2++) { pFriend = MercSlots[uiLoop2]; // if this merc is inactive, at base, on assignment, dead, unconscious if (!pFriend || (pFriend->stats.bLife < OKLIFE)) continue; // next merc // if this merc is not on my side, then he's NOT one of my friends // WE CAN'T AFFORD TO CONSIDER THE ENEMY OF MY ENEMY MY FRIEND, HERE! // ONLY IF WE ARE ACTUALLY OFFICIALLY CO-OPERATING TOGETHER (SAME SIDE) if (pFriend->aiData.bNeutral && !(pSoldier->ubCivilianGroup != NON_CIV_GROUP && pSoldier->ubCivilianGroup == pFriend->ubCivilianGroup)) { continue; // next merc } if (pSoldier->bSide != pFriend->bSide) continue; // next merc // THIS TEST IS INVALID IF A COMPUTER-TEAM IS PLAYING CO-OPERATIVELY // WITH A NON-COMPUTER TEAM SINCE THE OPPLISTS INVOLVED ARE NOT // UP-TO-DATE. THIS SITUATION IS CURRENTLY NOT POSSIBLE IN HTH/DG. // ALSO NOTE THAT WE COUNT US AS OUR (BEST) FRIEND FOR THESE CALCULATIONS // subtract HEARD_2_TURNS_AGO (which is negative) to make values start at 0 and // be positive otherwise iPercent = ThreatPercent[pFriend->aiData.bOppList[pOpponent->ubID] - OLDEST_HEARD_VALUE]; // reduce the percentage value based on how far away they are from the enemy, if they only hear him if (pFriend->aiData.bOppList[pOpponent->ubID] <= HEARD_LAST_TURN) { iPercent -= PythSpacesAway(pSoldier->sGridNo, pFriend->sGridNo) * 2; if (iPercent <= 0) { //ignore! continue; } } sFrndThreatValue = (iPercent * CalcManThreatValue(pFriend, pOpponent->sGridNo, FALSE, pSoldier)) / 100; //sprintf(tempstr,"Known by friend %s, opplist status %d, percent %d, threat = %d", // ExtMen[pFriend->ubID].name,pFriend->aiData.bOppList[pOpponent->ubID],ubPercent,sFrndThreatValue); //PopMessage(tempstr); // ADD this to our running total threatValue (increases my MORALE) // We multiply by sOppThreatValue to PRO-RATE this based on opponent's // threat value to ME personally. Divide later by sum of them all. iOurTotalThreat += sOppThreatValue * sFrndThreatValue; } // this could get slow if I have a lot of friends... //KeepInterfaceGoing(); } // if they are no threat whatsoever if (!iTheirTotalThreat) sMorale = 500; // our morale is just incredible else { // now divide sOutTotalThreat by sTheirTotalThreat to get the REAL value iOurTotalThreat /= iTheirTotalThreat; // calculate the morale (100 is even, < 100 is us losing, > 100 is good) sMorale = (INT16)((100 * iOurTotalThreat) / iTheirTotalThreat); } if (sMorale <= 25) // odds 1:4 or worse bMoraleCategory = MORALE_HOPELESS; else if (sMorale <= 50) // odds between 1:4 and 1:2 bMoraleCategory = MORALE_WORRIED; else if (sMorale <= 150) // odds between 1:2 and 3:2 bMoraleCategory = MORALE_NORMAL; else if (sMorale <= 300) // odds between 3:2 and 3:1 bMoraleCategory = MORALE_CONFIDENT; else // odds better than 3:1 bMoraleCategory = MORALE_FEARLESS; switch (pSoldier->aiData.bAttitude) { case DEFENSIVE: bMoraleCategory--; break; case BRAVESOLO: bMoraleCategory += 2; break; case BRAVEAID: bMoraleCategory += 2; break; case CUNNINGSOLO: break; case CUNNINGAID: break; case AGGRESSIVE: bMoraleCategory++; break; } // make idiot administrators much more aggressive if (pSoldier->ubSoldierClass == SOLDIER_CLASS_ADMINISTRATOR || pSoldier->ubSoldierClass == SOLDIER_CLASS_BANDIT) { bMoraleCategory += 2; } // if still full of energy if (pSoldier->bBreath > 75) bMoraleCategory++; else { // if getting a bit low on breath if (pSoldier->bBreath < 40) bMoraleCategory--; // if getting REALLY low on breath if (pSoldier->bBreath < 10) bMoraleCategory--; } // if still very healthy if (pSoldier->stats.bLife > 75) bMoraleCategory++; else { // if getting a bit low on life if (pSoldier->stats.bLife < 40) bMoraleCategory--; // if getting REALLY low on life if (pSoldier->stats.bLife < 20) bMoraleCategory--; } // if soldier is currently not under fire if (!pSoldier->aiData.bUnderFire) bMoraleCategory++; // if adjustments made it outside the allowed limits if (bMoraleCategory < MORALE_HOPELESS) bMoraleCategory = MORALE_HOPELESS; else { if (bMoraleCategory > MORALE_FEARLESS) bMoraleCategory = MORALE_FEARLESS; } // brave guys never get hopeless, at worst they get worried // SANDRO - on Insane difficulty enemy morale cannot go below worried if (bMoraleCategory == MORALE_HOPELESS) { if (pSoldier->aiData.bAttitude == BRAVESOLO || pSoldier->aiData.bAttitude == BRAVEAID || zDiffSetting[gGameOptions.ubDifficultyLevel].bEnemyMoraleWorried) bMoraleCategory = MORALE_WORRIED; } #ifdef DEBUGDECISIONS STR tempstr; sprintf(tempstr, "Morale = %d (category %d)\n", pSoldier->aiData.bMorale, bMoraleCategory); DebugAI(tempstr); #endif return(bMoraleCategory); } INT32 CalcManThreatValue( SOLDIERTYPE *pEnemy, INT32 sMyGrid, UINT8 ubReduceForCover, SOLDIERTYPE * pMe ) { INT32 iThreatValue = 0; BOOLEAN fForCreature = CREATURE_OR_BLOODCAT( pMe ); // If man is inactive, at base, on assignment, dead, unconscious if (!pEnemy->bActive || !pEnemy->bInSector || !pEnemy->stats.bLife) { // he's no threat at all, return a negative number iThreatValue = -999; return(iThreatValue); } // in boxing mode, let only a boxer be considered a threat. if ( (gTacticalStatus.bBoxingState == BOXING) && !BOXER(pEnemy) ) { iThreatValue = -999; return( iThreatValue ); } if (fForCreature) { // health (1-100) iThreatValue += pEnemy->stats.bLife; // bleeding (more attactive!) (1-100) iThreatValue += pEnemy->bBleeding; // decrease according to distance iThreatValue = (iThreatValue * 10) / (10 + PythSpacesAway( sMyGrid, pEnemy->sGridNo ) ); } else { // ADD twice the man's level (2-20) iThreatValue += EffectiveExpLevel(pEnemy); // SANDRO - find precise effective exp level // ADD man's total action points (10-35) iThreatValue += 25 * pEnemy->CalcActionPoints() / APBPConstants[AP_MAXIMUM]; // ADD 1/2 of man's current action points (4-17) iThreatValue += 25 * pEnemy->bActionPoints / APBPConstants[AP_MAXIMUM] / 2; // ADD 1/10 of man's current health (0-10) iThreatValue += (pEnemy->stats.bLife / 10); if (pEnemy->bAssignment < ON_DUTY ) { // ADD 1/4 of man's protection percentage (0-25) iThreatValue += ArmourPercent( pEnemy ) / 4; // ADD 1/5 of man's marksmanship skill (0-20) iThreatValue += (pEnemy->stats.bMarksmanship / 5); if ( Item[ pEnemy->inv[HANDPOS].usItem ].usItemClass & IC_WEAPON ) { // ADD the deadliness of the item(weapon) he's holding (0-50) iThreatValue += Weapon[pEnemy->inv[HANDPOS].usItem].ubDeadliness; } } // SUBTRACT 1/5 of man's bleeding (0-20) iThreatValue -= (pEnemy->bBleeding / 5); // SUBTRACT 1/10 of man's breath deficiency (0-10) iThreatValue -= ((100 - pEnemy->bBreath) / 10); // SUBTRACT man's current shock value iThreatValue -= pEnemy->aiData.bShock; } // if I have a specifically defined spot where I'm at (sometime I don't!) if (!TileIsOutOfBounds(sMyGrid)) { // ADD 10% if man's already been shooting at me if (pEnemy->sLastTarget == sMyGrid) { iThreatValue += (iThreatValue / 10); } else { // ADD 5% if man's already facing me if (pEnemy->ubDirection == GetDirectionFromCenterCellXYGridNo(pEnemy->sGridNo, sMyGrid)) { iThreatValue += (iThreatValue / 20); } } } // if this man is conscious if (pEnemy->stats.bLife >= OKLIFE) { // and we were told to reduce threat for my cover if (ubReduceForCover && (!TileIsOutOfBounds(sMyGrid))) { // Reduce iThreatValue to same % as the chance HE has shoot through at ME //iThreatValue = (iThreatValue * ChanceToGetThrough( pEnemy, myGrid, FAKE, ACTUAL, TESTWALLS, 9999, M9PISTOL, NOT_FOR_LOS)) / 100; //iThreatValue = (iThreatValue * SoldierTo3DLocationChanceToGetThrough( pEnemy, myGrid, FAKE, ACTUAL, TESTWALLS, 9999, M9PISTOL, NOT_FOR_LOS)) / 100; iThreatValue = (iThreatValue * SoldierToLocationChanceToGetThrough( pEnemy, sMyGrid, pMe->pathing.bLevel, 0, pMe->ubID ) ) / 100; } } else { // if he's still something of a threat if (iThreatValue > 0) { // drastically reduce his threat value (divide by 5 to 18) iThreatValue /= (4 + (OKLIFE - pEnemy->stats.bLife)); } } // threat value of any opponent can never drop below 1 if (iThreatValue < 1) { iThreatValue = 1; } //sprintf(tempstr,"%s's iThreatValue = ",pEnemy->name); //NumMessage(tempstr,iThreatValue); #ifdef BETAVERSION // unnecessary for real release // NOTE: maximum is about 200 for a healthy Mike type with a mortar! if (iThreatValue > 250) { sprintf(tempstr,"CalcManThreatValue: WARNING - %d has a very high threat value of %d",pEnemy->ubID,iThreatValue); #ifdef RECORDNET fprintf(NetDebugFile,"\t%s\n",tempstr); #endif #ifdef TESTVERSION PopMessage(tempstr); #endif } #endif return(iThreatValue); } INT16 RoamingRange(SOLDIERTYPE *pSoldier, INT32 * pusFromGridNo) { BOOL OppPosKnown = FALSE; if (CREATURE_OR_BLOODCAT(pSoldier)) { if (pSoldier->aiData.bAlertStatus > STATUS_YELLOW) { *pusFromGridNo = pSoldier->sGridNo; // from current position! return(MAX_ROAMING_RANGE); } } // sevenfm: no limits for zombies if (pSoldier->IsZombie()) { return(MAX_ROAMING_RANGE); } if (pSoldier->aiData.bOrders == POINTPATROL || pSoldier->aiData.bOrders == RNDPTPATROL) { // roam near NEXT PATROL POINT, not from where merc starts out *pusFromGridNo = pSoldier->aiData.sPatrolGrid[pSoldier->aiData.bNextPatrolPnt]; } else { // roam around where mercs started //*pusFromGridNo = pSoldier->sInitialGridNo; *pusFromGridNo = pSoldier->aiData.sPatrolGrid[0]; } //Do we know about any opponent? for (UINT16 oppID = 0; oppID < MAX_NUM_SOLDIERS; oppID++) { if (pSoldier->aiData.bOppList[oppID] != NOT_HEARD_OR_SEEN && gbPublicOpplist[pSoldier->bTeam][oppID] != NOT_HEARD_OR_SEEN) { OppPosKnown = TRUE; break; } } switch (pSoldier->aiData.bOrders) { // JA2 GOLD: give non-NPCs a 5 tile roam range for cover in combat when being shot at // anv: and tanks who are technically NPCs case STATIONARY: if ((pSoldier->ubProfile != NO_PROFILE && !ARMED_VEHICLE(pSoldier)) || (pSoldier->aiData.bAlertStatus < STATUS_BLACK && !(pSoldier->aiData.bUnderFire))) { return(0); } else { return(5); } case ONGUARD: return(5); case CLOSEPATROL: if (pSoldier->aiData.bAlertStatus < STATUS_RED) { return(5); } else { if (!OppPosKnown) { return(15); } else { return(30); //return( MAX_ROAMING_RANGE ); } } case POINTPATROL: if (pSoldier->aiData.bAlertStatus < STATUS_RED) { return(10); } else { if (!OppPosKnown) { return(20); } else { return(40); //return( MAX_ROAMING_RANGE ); } } // from nextPatrolGrid, not whereIWas case RNDPTPATROL: if (pSoldier->aiData.bAlertStatus < STATUS_RED) { return(10); } else { if (!OppPosKnown) { return(20); } else { //return( 40 ); return(MAX_ROAMING_RANGE); } }// from nextPatrolGrid, not whereIWas case FARPATROL: if (pSoldier->aiData.bAlertStatus < STATUS_RED) { return(15); } else { if (!OppPosKnown) { return(30); } else { return(MAX_ROAMING_RANGE); } } case ONCALL: if (pSoldier->aiData.bAlertStatus < STATUS_RED) { return(10); } else { if (!OppPosKnown) { return(30); } else { //return(50); return(MAX_ROAMING_RANGE); } } case SEEKENEMY: *pusFromGridNo = pSoldier->sGridNo; // from current position! return(MAX_ROAMING_RANGE); case SNIPER: return ( 5 ); default: #ifdef BETAVERSION sprintf(tempstr, "%s has invalid orders = %d", pSoldier->GetName(), pSoldier->aiData.bOrders); PopMessage(tempstr); #endif return(0); } } void RearrangePocket(SOLDIERTYPE *pSoldier, INT8 bPocket1, INT8 bPocket2, UINT8 bPermanent) { DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"RearrangePocket"); // NB there's no such thing as a temporary swap for now... // 0verhaul: There is now! If not permanent, don't lose weapon ready status because the // weapon will be restored after the trial situation is finished. //SwapObjs( &(pSoldier->inv[bPocket1]), &(pSoldier->inv[bPocket2]) ); SwapObjs( pSoldier, bPocket1, bPocket2, bPermanent ); DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"RearrangePocket done"); } BOOLEAN FindBetterSpotForItem( SOLDIERTYPE * pSoldier, INT8 bSlot ) { // looks for a place in the slots to put an item in a hand or armour // position, and moves it there. if (bSlot >= BIGPOCKSTART) { return( FALSE ); } if (pSoldier->inv[bSlot].exists() == false) { // well that's just fine then! return( TRUE ); } if(FitsInSmallPocket(&pSoldier->inv[bSlot]) == false) { // then we're looking for a big pocket bSlot = FindEmptySlotWithin( pSoldier, BIGPOCKSTART, MEDPOCKFINAL ); } else { // try a small pocket first bSlot = FindEmptySlotWithin( pSoldier, SMALLPOCKSTART, NUM_INV_SLOTS ); if (bSlot == NO_SLOT) { bSlot = FindEmptySlotWithin( pSoldier, BIGPOCKSTART, MEDPOCKFINAL ); } } if (bSlot == NO_SLOT) { return( FALSE ); } DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"findbetterspotforitem: swapping items"); RearrangePocket(pSoldier, HANDPOS, bSlot, FOREVER ); return( TRUE ); } UINT8 GetTraversalQuoteActionID( INT8 bDirection ) { switch( bDirection ) { case NORTHEAST: // east return( QUOTE_ACTION_ID_TRAVERSE_EAST ); case SOUTHEAST: // south return( QUOTE_ACTION_ID_TRAVERSE_SOUTH ); case SOUTHWEST: // west return( QUOTE_ACTION_ID_TRAVERSE_WEST ); case NORTHWEST: // north return( QUOTE_ACTION_ID_TRAVERSE_NORTH ); default: return( 0 ); } } UINT8 SoldierDifficultyLevel( SOLDIERTYPE * pSoldier ) { INT8 bDifficultyBase; INT8 bDifficulty; DebugMsg(TOPIC_JA2AI,DBG_LEVEL_3,String("SoldierDifficultyLevel")); // difficulty modifier ranges from 0 to 100 // and we want to end up with a number between 0 and 4 (4=hardest) // to a base of 1, divide by 34 to get a range from 1 to 3 bDifficultyBase = 1 + ( CalcDifficultyModifier( pSoldier->ubSoldierClass ) / 34 ); switch( pSoldier->ubSoldierClass ) { case SOLDIER_CLASS_ADMINISTRATOR: case SOLDIER_CLASS_BANDIT: bDifficulty = bDifficultyBase - 1; break; case SOLDIER_CLASS_ARMY: bDifficulty = bDifficultyBase; break; case SOLDIER_CLASS_ROBOT: case SOLDIER_CLASS_ELITE: bDifficulty = bDifficultyBase + 1; break; // hard code militia; case SOLDIER_CLASS_GREEN_MILITIA: bDifficulty = 2; break; case SOLDIER_CLASS_REG_MILITIA: bDifficulty = 3; break; case SOLDIER_CLASS_ELITE_MILITIA: bDifficulty = 4; break; case SOLDIER_CLASS_ZOMBIE: bDifficulty = bDifficultyBase; break; default: if (pSoldier->bTeam == CREATURE_TEAM) { bDifficulty = bDifficultyBase + pSoldier->pathing.bLevel / 4; } else // civ... { bDifficulty = (bDifficultyBase + pSoldier->pathing.bLevel / 4) - 1; } break; } bDifficulty = __max( bDifficulty, 0 ); bDifficulty = __min( bDifficulty, 4 ); return( (UINT8) bDifficulty ); } BOOLEAN ValidCreatureTurn( SOLDIERTYPE * pCreature, INT8 bNewDirection ) { INT8 bDirChange; INT8 bTempDir; INT8 bLoop; BOOLEAN fFound; bDirChange = (INT8) QuickestDirection( pCreature->ubDirection, bNewDirection ); for( bLoop = 0; bLoop < 2; bLoop++ ) { fFound = TRUE; bTempDir = pCreature->ubDirection; do { bTempDir += bDirChange; if (bTempDir < NORTH) { bTempDir = NORTHWEST; } else if (bTempDir > NORTHWEST) { bTempDir = NORTH; } if (!pCreature->InternalIsValidStance( bTempDir, ANIM_STAND )) { fFound = FALSE; break; } } while ( bTempDir != bNewDirection ); if ( fFound ) { break; } else if ( bLoop > 0 ) { // can't find a dir! return( FALSE ); } else { // try the other direction bDirChange = bDirChange * -1; } } return( TRUE ); } INT32 RangeChangeDesire( SOLDIERTYPE * pSoldier ) { INT32 iRangeFactorMultiplier; iRangeFactorMultiplier = pSoldier->aiData.bAIMorale - 1; // sevenfm: retreat if (pSoldier->RetreatCounterValue() > 0) { return 0; } // civilians only run away if (pSoldier->aiData.bNeutral) { return 0; } INT8 bBonus = -1; if (IS_MERC_BODY_TYPE(pSoldier)) { if (!AICheckHasGun(pSoldier)) bBonus = 2; // if we have no weapons, try to get closer to enemy else if (GuySawEnemy(pSoldier, SEEN_LAST_TURN) && AICheckShortWeaponRange(pSoldier)) bBonus = 1; // bonus if weapon range is short } switch (pSoldier->aiData.bAttitude) { case DEFENSIVE: iRangeFactorMultiplier += max(-1, bBonus); break; case BRAVESOLO: iRangeFactorMultiplier += max(2, bBonus); break; case BRAVEAID: iRangeFactorMultiplier += max(2, bBonus); break; case CUNNINGSOLO: iRangeFactorMultiplier += max(0, bBonus); break; case CUNNINGAID: iRangeFactorMultiplier += max(0, bBonus); break; case ATTACKSLAYONLY: case AGGRESSIVE: iRangeFactorMultiplier += max(1, bBonus); break; } if ( gTacticalStatus.bConsNumTurnsWeHaventSeenButEnemyDoes > 0 ) { iRangeFactorMultiplier += gTacticalStatus.bConsNumTurnsWeHaventSeenButEnemyDoes; } return iRangeFactorMultiplier; } BOOLEAN ArmySeesOpponents( void ) { SOLDIERTYPE * pSoldier; for ( SoldierID cnt = gTacticalStatus.Team[ ENEMY_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ ENEMY_TEAM ].bLastID; ++cnt ) { pSoldier = cnt; if ( pSoldier->bActive && pSoldier->bInSector && pSoldier->stats.bLife >= OKLIFE && pSoldier->aiData.bOppCnt > 0 ) { return( TRUE ); } } return( FALSE ); } #ifdef DEBUGDECISIONS void AIPopMessage ( STR16 str ) { DebugAI(str); } void AIPopMessage ( const STR8 str ) { STR tempstr; sprintf( tempstr,"%s", str); DebugAI(tempstr); } void AINumMessage(const STR8 str, INT32 num) { STR tempstr; sprintf( tempstr,"%s %d", str, num); DebugAI(tempstr); } void AINameMessage(SOLDIERTYPE * pSoldier,const STR8 str,INT32 num) { STR tempstr; sprintf( tempstr,"%d %s %d",pSoldier->GetName() , str, num); DebugAI( tempstr ); } #endif ///////////////////////////////////////////////////////////////////////////////////////////////// // HEADROCK: // // The following function(s) are part of my half-assed attempt to have the AI analyze the tactical // situation, by comparing (known) squad sizes, the state of all combatants, and the orders of all // friendlies. The idea is to return a value called "TacticalSituation" which can tell a combatant // whether he should try to undertake a smarter course of action. ///////////////////////////////////////////////////////////////////////////////////////////////// /* INT16 AssessTacticalSituation( INT8 bTeam ) { UINT16 ubFriendlyTeamTacticalValue = 0; UINT16 ubEnemyTeamTacticalValue = 0; UINT8 ubSoldierTacticalThreat; INT16 ubTacticalSituation; UINT16 cnt; SOLDIERTYPE * pSoldier; // begin loop through all MERCs. for ( cnt = gTacticalStatus.Team[ OUR_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ OUR_TEAM ].bLastID; cnt++ ) { pSoldier = MercPtrs[ cnt ]; ubSoldierTacticalThreat = CalcStraightThreatValue( pSoldier ); // Player-controlled Mercs are 1.5 times more threatening than AIs if (pSoldier->flags.uiStatusFlags & SOLDIER_PC) ubSoldierTacticalThreat = (UINT8)((float)ubSoldierTacticalThreat * 1.5); // Assess Threat if (bTeam == OUR_TEAM || bTeam == MILITIA_TEAM) { // Friendly! ubFriendlyTeamTacticalValue += ubSoldierTacticalThreat; } else { // Enemy! if ( TeamSeesOpponent( ENEMY_TEAM, pSoldier ) ) ubEnemyTeamTacticalValue += ubSoldierTacticalThreat; } } // begin loop through all Militia. for ( cnt = gTacticalStatus.Team[ MILITIA_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ MILITIA_TEAM ].bLastID; cnt++ ) { pSoldier = MercPtrs[ cnt ]; ubSoldierTacticalThreat = CalcStraightThreatValue( pSoldier ); // Assess Threat if (bTeam == OUR_TEAM || bTeam == MILITIA_TEAM) { // Friendly! ubFriendlyTeamTacticalValue += ubSoldierTacticalThreat; } else { // Enemy! if ( TeamSeesOpponent( ENEMY_TEAM, pSoldier ) ) ubEnemyTeamTacticalValue += ubSoldierTacticalThreat; } } // begin loop through all Enemies. for ( cnt = gTacticalStatus.Team[ ENEMY_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ ENEMY_TEAM ].bLastID; cnt++ ) { pSoldier = MercPtrs[ cnt ]; ubSoldierTacticalThreat = CalcStraightThreatValue( pSoldier ); // Assess Threat if (bTeam == ENEMY_TEAM) { // Friendly! ubFriendlyTeamTacticalValue += ubSoldierTacticalThreat; } else { // Enemy! if ( TeamSeesOpponent( OUR_TEAM, pSoldier ) || TeamSeesOpponent ( MILITIA_TEAM, pSoldier) ) ubEnemyTeamTacticalValue += ubSoldierTacticalThreat; } } ubTacticalSituation = ubEnemyTeamTacticalValue - ubFriendlyTeamTacticalValue; return (ubTacticalSituation); } */ // HEADROCK: Function to check whether a team can see the specified soldier. BOOLEAN TeamSeesOpponent( INT8 bTeam, SOLDIERTYPE * pOpponent ) { SOLDIERTYPE * pSoldier; SoldierID cnt; // This assertion can be safely removed, assuming the program does what it should. It simply checks // whether the "opponent" is on the same team being checked. That should be avoided when calling this // function. Assert( pOpponent->bTeam != bTeam ); // We're checking Merc/Militia visibility if (bTeam == OUR_TEAM || bTeam == MILITIA_TEAM ) { for ( cnt = gTacticalStatus.Team[ MILITIA_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ MILITIA_TEAM ].bLastID; ++cnt ) { pSoldier = cnt; if (pSoldier->bActive && pSoldier->bInSector && pSoldier->stats.bLife >= OKLIFE) { if (pSoldier->aiData.bOppList[ pOpponent->ubID ] == SEEN_CURRENTLY) return ( TRUE ); } } for ( cnt = gTacticalStatus.Team[ OUR_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ OUR_TEAM ].bLastID; ++cnt ) { pSoldier = cnt; if (pSoldier->bActive && pSoldier->bInSector && pSoldier->stats.bLife >= OKLIFE) { // This assertion can be safely removed, assuming the program does what it should. It simply checks // whether the "opponent" is on the same team being checked. That should be avoided when calling this // function. //Assert( pOpponent->bSide != bSide ); if (pSoldier->aiData.bOppList[ pOpponent->ubID ] == SEEN_CURRENTLY) return ( TRUE ); } } return ( FALSE ); } // Check enemy visibility else if (bTeam == ENEMY_TEAM) { for ( cnt = gTacticalStatus.Team[ ENEMY_TEAM ].bFirstID; cnt <= gTacticalStatus.Team[ ENEMY_TEAM ].bLastID; ++cnt ) { pSoldier = cnt; if (pSoldier->bActive && pSoldier->bInSector && pSoldier->stats.bLife >= OKLIFE) { // This assertion can be safely removed, assuming the program does what it should. It simply checks // whether the "opponent" is on the same team being checked. That should be avoided when calling this // function. //Assert( pOpponent->bSide != bSide ); if (pSoldier->aiData.bOppList[ pOpponent->ubID ] == SEEN_CURRENTLY) return ( TRUE ); } } return ( FALSE ); } else return (FALSE); } // HEADROCK: Function to assess an enemy's threat value without "me" argument. INT32 CalcStraightThreatValue( SOLDIERTYPE *pEnemy ) { INT32 iThreatValue = 0; // If man is inactive, at base, on assignment, dead, unconscious if (!pEnemy->bActive || !pEnemy->bInSector || !pEnemy->stats.bLife ) { // he's no threat at all, return a negative number iThreatValue = 0; return(iThreatValue); } else { // ADD twice the man's level (2-20) iThreatValue += EffectiveExpLevel(pEnemy); // SANDRO - find precise effective exp level // ADD man's total action points (10-35) iThreatValue += 25 * pEnemy->CalcActionPoints() / APBPConstants[AP_MAXIMUM]; // ADD 1/2 of man's current action points (4-17) iThreatValue += 25 * pEnemy->bActionPoints / APBPConstants[AP_MAXIMUM] / 2; // ADD 1/10 of man's current health (0-10) iThreatValue += (pEnemy->stats.bLife / 10); if (pEnemy->bAssignment < ON_DUTY ) { // ADD 1/4 of man's protection percentage (0-25) iThreatValue += ArmourPercent( pEnemy ) / 4; // ADD 1/5 of man's marksmanship skill (0-20) iThreatValue += (pEnemy->stats.bMarksmanship / 5); if ( Item[ pEnemy->inv[HANDPOS].usItem ].usItemClass & IC_WEAPON ) { // ADD the deadliness of the item(weapon) he's holding (0-50) iThreatValue += Weapon[pEnemy->inv[HANDPOS].usItem].ubDeadliness; } } // SUBTRACT 1/5 of man's bleeding (0-20) iThreatValue -= (pEnemy->bBleeding / 5); // SUBTRACT 1/10 of man's breath deficiency (0-10) iThreatValue -= ((100 - pEnemy->bBreath) / 10); // SUBTRACT man's current shock value iThreatValue -= pEnemy->aiData.bShock; } // if this man is conscious if (pEnemy->stats.bLife < OKLIFE) { // if he's still something of a threat if (iThreatValue > 0) { // drastically reduce his threat value (divide by 5 to 18) iThreatValue /= (4 + (OKLIFE - pEnemy->stats.bLife)); } } // threat value of any opponent can never drop below 1 if (iThreatValue < 0) { iThreatValue = 0; } return(iThreatValue); } // Flugente: get the id of the closest soldier with a specific flag that we can currently see SoldierID GetClosestFlaggedSoldierID( SOLDIERTYPE * pSoldier, INT16 aRange, UINT8 auTeam, UINT32 aFlag, BOOLEAN fCheckSight ) { SoldierID id = NOBODY; SOLDIERTYPE * pFriend; INT16 range = aRange; // go through each soldier, looking for "friends" (soldiers on same team) for ( SoldierID uiLoop = gTacticalStatus.Team[ auTeam ].bFirstID; uiLoop <= gTacticalStatus.Team[ auTeam ].bLastID; ++uiLoop) { pFriend = uiLoop; // if this merc is inactive, not in sector, or dead if (!pFriend) continue; // skip ourselves if (pFriend->ubID == pSoldier->ubID) continue; // must be on the same level if ( pFriend->pathing.bLevel != pSoldier->pathing.bLevel ) continue; // this is not for tanks if ( ARMED_VEHICLE( pFriend ) || ENEMYROBOT( pFriend )) continue; // skip if this guy is dead if ( pFriend->stats.bLife <= 0 ) continue; // check for flag if ( !(pFriend->usSoldierFlagMask & aFlag) ) continue; // are we close enough? INT16 friendrange = SpacesAway(pSoldier->sGridNo, pFriend->sGridNo); if ( friendrange < range) { // can we see this guy? if ( !fCheckSight || SoldierTo3DLocationLineOfSightTest( pSoldier, pFriend->sGridNo, pSoldier->pathing.bLevel, 3, TRUE, CALC_FROM_WANTED_DIR ) ) { range = friendrange; id = pFriend->ubID; } } } return id; } // get the id of the closest soldier (closer than x tiles) of a specific team that is wounded that we can currently see SoldierID GetClosestWoundedSoldierID( SOLDIERTYPE * pSoldier, INT16 aRange, UINT8 auTeam ) { SoldierID id = NOBODY; SOLDIERTYPE * pFriend; INT16 range = aRange; // go through each soldier, looking for "friends" (soldiers on same team) for ( SoldierID uiLoop = gTacticalStatus.Team[ auTeam ].bFirstID; uiLoop <= gTacticalStatus.Team[ auTeam ].bLastID; ++uiLoop) { pFriend = uiLoop; // if this merc is inactive, not in sector, or dead if (!pFriend) continue; // skip ourselves (if not allowed) if ( !gGameExternalOptions.fEnemyMedicsHealSelf && pFriend->ubID == pSoldier->ubID) continue; // must be on the same level if ( pFriend->pathing.bLevel != pSoldier->pathing.bLevel ) continue; // this is not for tanks if ( ARMED_VEHICLE( pFriend ) || ENEMYROBOT( pFriend ) ) continue; // skip if this guy is dead, or not wounded (enough) if ( pFriend->stats.bLife <= 0 || pFriend->iHealableInjury < gGameExternalOptions.sEnemyMedicsWoundMinAmount ) continue; // are we close enough? INT16 friendrange = SpacesAway(pSoldier->sGridNo, pFriend->sGridNo); if ( friendrange < range) { // can we see this guy? //if ( SoldierTo3DLocationLineOfSightTest( pSoldier, pFriend->sGridNo, pSoldier->pathing.bLevel, 3, TRUE, CALC_FROM_WANTED_DIR ) ) { range = friendrange; id = pFriend->ubID; } } } return id; } // get the id of the closest medic (closer than x tiles) of a specific team SoldierID GetClosestMedicSoldierID( SOLDIERTYPE * pSoldier, INT16 aRange, UINT8 auTeam ) { SoldierID id = NOBODY; SOLDIERTYPE * pFriend; INT16 range = aRange; // go through each soldier, looking for "friends" (soldiers on same team) for ( SoldierID uiLoop = gTacticalStatus.Team[ auTeam ].bFirstID; uiLoop <= gTacticalStatus.Team[ auTeam ].bLastID; ++uiLoop) { pFriend = uiLoop; // if this merc is inactive, not in sector, or dead if (!pFriend) continue; // skip ourselves (we seek OTHER people) if ( pFriend->ubID == pSoldier->ubID) continue; // must be on the same level if ( pFriend->pathing.bLevel != pSoldier->pathing.bLevel ) continue; // this is not for tanks if ( ARMED_VEHICLE( pFriend ) || ENEMYROBOT( pFriend ) ) continue; // skip this guy if he is dead or unconscious if ( pFriend->stats.bLife < OKLIFE ) continue; // skip if this guy if he is no medic if ( !pFriend->CanMedicAI() ) continue; // are we close enough? INT16 friendrange = SpacesAway(pSoldier->sGridNo, pFriend->sGridNo); if ( friendrange < range) { // can we see this guy? //if ( SoldierTo3DLocationLineOfSightTest( pSoldier, pFriend->sGridNo, pSoldier->pathing.bLevel, 3, TRUE, CALC_FROM_WANTED_DIR ) ) { range = friendrange; id = pFriend->ubID; } } } return id; } // sevenfm: define normal vision limits for day/night INT16 MaxNormalVisionDistance( void ) { if( NightTime() ) { return gGameExternalOptions.ubStraightSightRange * STRAIGHT_RATIO; } return gGameExternalOptions.ubStraightSightRange * 2 * STRAIGHT_RATIO; } // sevenfm: check friendly soldiers between me and noise gridno // count only friends that are active and not stationary/onguard/sniper UINT16 CountFriendsInDirection( SOLDIERTYPE *pSoldier, INT32 sTargetGridNo ) { SOLDIERTYPE * pFriend; UINT8 ubFriendDir, ubMyDir; UINT16 ubFriends = 0; CHECKF(pSoldier); ubMyDir = GetDirectionFromCenterCellXYGridNo(sTargetGridNo, pSoldier->sGridNo); // Run through each friendly. for ( SoldierID iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; ++iCounter ) { pFriend = iCounter; ubFriendDir = GetDirectionFromCenterCellXYGridNo(sTargetGridNo, pFriend->sGridNo); if (pFriend != pSoldier && pFriend->bActive && pFriend->stats.bLife >= OKLIFE && pFriend->stats.bLife >= pFriend->stats.bLifeMax/2 && pFriend->aiData.bOrders > ONGUARD && pFriend->aiData.bOrders != SNIPER && (ubFriendDir == ubMyDir || ubFriendDir == gOneCDirection[ubMyDir] || ubFriendDir == gOneCCDirection[ubMyDir]) && PythSpacesAway( sTargetGridNo, pFriend->sGridNo) < PythSpacesAway(sTargetGridNo, pSoldier->sGridNo) ) { ubFriends++; } } return ubFriends; } // sevenfm: count nearby friend soldiers UINT16 CountNearbyFriends( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance ) { SOLDIERTYPE * pFriend; UINT16 ubFriendCount = 0; // safety check if( !pSoldier ) return 0; // Run through each friendly. for ( SoldierID iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; ++iCounter ) { pFriend = iCounter; // Make sure that character is alive, not too shocked, and conscious, and of higher experience level // than the character being suppressed. if (pFriend != pSoldier && pFriend->bActive && pFriend->stats.bLife >= OKLIFE && PythSpacesAway( sGridNo, pFriend->sGridNo ) <= ubDistance ) { ubFriendCount++; } } return ubFriendCount; } // sevenfm: new AI morale calculation INT8 CalcMoraleNew(SOLDIERTYPE *pSoldier) { UINT32 uiLoop, uiLoop2; INT32 iOurTotalThreat = 0, iTheirTotalThreat = 0; INT16 sOppThreatValue, sFrndThreatValue, sMorale; INT32 iPercent; INT8 bMostRecentOpplistValue; INT8 bMoraleCategory; UINT8 *pSeenOpp; //,*friendOlPtr; INT8 *pbPersOL, *pbPublOL; SOLDIERTYPE *pOpponent, *pFriend; // zombies always have high morale if (pSoldier->IsZombie()) { return MORALE_FEARLESS; } // sevenfm: retreat, also when blinded if (pSoldier->RetreatCounterValue() > 0 || pSoldier->bBlindedCounter > 0) { return MORALE_WORRIED; } // sevenfm: neutrals always have low AI morale even if they have weapons (so they run from enemy) if (pSoldier->aiData.bNeutral) { return(MORALE_WORRIED); } // if army guy has NO weapons left then panic! if ( pSoldier->bTeam == ENEMY_TEAM || !pSoldier->aiData.bNeutral ) { if ( FindAIUsableObjClass( pSoldier, IC_WEAPON ) == NO_SLOT ) { // sevenfm: instead of leaving sector, try to attack with hands/knife return( MORALE_FEARLESS ); //return( MORALE_HOPELESS ); } } // hang pointers to my personal opplist, my team's public opplist, and my // list of previously seen opponents pSeenOpp = (UINT8 *) &(gbSeenOpponents[pSoldier->ubID][0]); // loop through every one of my possible opponents for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { pOpponent = MercSlots[ uiLoop ]; // if this merc is inactive, at base, on assignment, dead, unconscious if (!pOpponent || (pOpponent->stats.bLife < OKLIFE)) continue; // next merc // if this merc is neutral/on same side, he's not an opponent, skip him! if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide)) continue; // next merc // Special stuff for Carmen the bounty hunter if (pSoldier->aiData.bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != SLAY) { continue; // next opponent } pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID; pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID; pSeenOpp = (UINT8 *)gbSeenOpponents[pSoldier->ubID] + pOpponent->ubID; // if this opponent is unknown to me personally AND unknown to my team, too if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN)) { // if I have never seen him before anywhere in this sector, either if (!(*pSeenOpp)) continue; // next merc // have seen him in the past, so he remains something of a threat bMostRecentOpplistValue = 0; // uses the free slot for 0 opplist } else // decide which opplist is more current { // if personal knowledge is more up to date or at least equal if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) || (*pbPersOL == *pbPublOL)) bMostRecentOpplistValue = *pbPersOL; // use personal else bMostRecentOpplistValue = *pbPublOL; // use public } iPercent = ThreatPercent[bMostRecentOpplistValue - OLDEST_HEARD_VALUE]; sOppThreatValue = (iPercent * CalcManThreatValue(pOpponent,pSoldier->sGridNo,FALSE,pSoldier)) / 100; //sprintf(tempstr,"Known opponent %s, opplist status %d, percent %d, threat = %d", // ExtMen[pOpponent->ubID].name,ubMostRecentOpplistValue,ubPercent,sOppThreatValue); //PopMessage(tempstr); // ADD this to their running total threatValue (decreases my MORALE) iTheirTotalThreat += sOppThreatValue; //NumMessage("Their TOTAL threat now = ",sTheirTotalThreat); // NOW THE FUN PART: SINCE THIS OPPONENT IS KNOWN TO ME IN SOME WAY, // ANY FRIENDS OF MINE THAT KNOW ABOUT HIM BOOST MY MORALE. SO, LET'S GO // THROUGH THEIR PERSONAL OPPLISTS AND CHECK WHICH OF MY FRIENDS KNOW // SOMETHING ABOUT HIM AND WHAT THEIR THREAT VALUE TO HIM IS. for (uiLoop2 = 0; uiLoop2 < guiNumMercSlots; uiLoop2++) { pFriend = MercSlots[ uiLoop2 ]; // if this merc is inactive, at base, on assignment, dead, unconscious if (!pFriend || (pFriend->stats.bLife < OKLIFE)) continue; // next merc // if this merc is not on my side, then he's NOT one of my friends // WE CAN'T AFFORD TO CONSIDER THE ENEMY OF MY ENEMY MY FRIEND, HERE! // ONLY IF WE ARE ACTUALLY OFFICIALLY CO-OPERATING TOGETHER (SAME SIDE) if ( pFriend->aiData.bNeutral && !( pSoldier->ubCivilianGroup != NON_CIV_GROUP && pSoldier->ubCivilianGroup == pFriend->ubCivilianGroup ) ) { continue; // next merc } if ( pSoldier->bSide != pFriend->bSide ) continue; // next merc // THIS TEST IS INVALID IF A COMPUTER-TEAM IS PLAYING CO-OPERATIVELY // WITH A NON-COMPUTER TEAM SINCE THE OPPLISTS INVOLVED ARE NOT // UP-TO-DATE. THIS SITUATION IS CURRENTLY NOT POSSIBLE IN HTH/DG. // ALSO NOTE THAT WE COUNT US AS OUR (BEST) FRIEND FOR THESE CALCULATIONS // subtract HEARD_2_TURNS_AGO (which is negative) to make values start at 0 and // be positive otherwise iPercent = ThreatPercent[pFriend->aiData.bOppList[pOpponent->ubID] - OLDEST_HEARD_VALUE]; // reduce the percentage value based on how far away they are from the enemy, if they only hear him if ( pFriend->aiData.bOppList[ pOpponent->ubID ] <= HEARD_LAST_TURN ) { iPercent -= PythSpacesAway( pSoldier->sGridNo, pFriend->sGridNo ) * 2; if ( iPercent <= 0 ) { //ignore! continue; } } sFrndThreatValue = (iPercent * CalcManThreatValue(pFriend,pOpponent->sGridNo,FALSE,pSoldier)) / 100; //sprintf(tempstr,"Known by friend %s, opplist status %d, percent %d, threat = %d", // ExtMen[pFriend->ubID].name,pFriend->aiData.bOppList[pOpponent->ubID],ubPercent,sFrndThreatValue); //PopMessage(tempstr); // ADD this to our running total threatValue (increases my MORALE) // We multiply by sOppThreatValue to PRO-RATE this based on opponent's // threat value to ME personally. Divide later by sum of them all. iOurTotalThreat += sOppThreatValue * sFrndThreatValue; } // this could get slow if I have a lot of friends... //KeepInterfaceGoing(); } // if they are no threat whatsoever if (!iTheirTotalThreat) sMorale = 500; // our morale is just incredible else { // now divide sOutTotalThreat by sTheirTotalThreat to get the REAL value iOurTotalThreat /= iTheirTotalThreat; // calculate the morale (100 is even, < 100 is us losing, > 100 is good) sMorale = (INT16) ((100 * iOurTotalThreat) / iTheirTotalThreat); } if (sMorale <= 25) // odds 1:4 or worse bMoraleCategory = MORALE_HOPELESS; else if (sMorale <= 50) // odds between 1:4 and 1:2 bMoraleCategory = MORALE_WORRIED; else if (sMorale <= 150) // odds between 1:2 and 3:2 bMoraleCategory = MORALE_NORMAL; else if (sMorale <= 300) // odds between 3:2 and 3:1 bMoraleCategory = MORALE_CONFIDENT; else // odds better than 3:1 bMoraleCategory = MORALE_FEARLESS; switch (pSoldier->aiData.bAttitude) { case DEFENSIVE: bMoraleCategory--; break; case BRAVESOLO: bMoraleCategory += 2; break; case BRAVEAID: bMoraleCategory += 2; break; case CUNNINGSOLO: break; case CUNNINGAID: break; case AGGRESSIVE: bMoraleCategory++; break; } // make idiot administrators more aggressive if ( pSoldier->ubSoldierClass == SOLDIER_CLASS_ADMINISTRATOR || pSoldier->ubSoldierClass == SOLDIER_CLASS_BANDIT ) { bMoraleCategory += 2; } // if have good health if( pSoldier->stats.bLife > pSoldier->stats.bLifeMax / 2 ) { bMoraleCategory++; } // bad health if( pSoldier->stats.bLife < pSoldier->stats.bLifeMax / 4 ) { bMoraleCategory--; } // good breath if( pSoldier->bBreath > 50 ) { bMoraleCategory++; } // bad breath if( pSoldier->bBreath < 25 ) { bMoraleCategory--; } // if not under fire - attack if( !pSoldier->aiData.bUnderFire ) { bMoraleCategory++; } // count friends that flank around the same spot if( CountFriendsFlankSameSpot( pSoldier ) == 0 ) { bMoraleCategory ++; } INT32 sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); // if last attack of this soldier hit enemy - increase morale if( pSoldier->aiData.bLastAttackHit ) { bMoraleCategory++; } // if some friend hit enemy - increase morale if (CountNearbyFriendsLastAttackHit(pSoldier, pSoldier->sGridNo, TACTICAL_RANGE / 4) > 0) { bMoraleCategory++; } // limit AI morale when soldier is under heavy fire /*if (pSoldier->ShockLevelPercent() > 75) bMoraleCategory = min(bMoraleCategory, MORALE_NORMAL); else if (pSoldier->ShockLevelPercent() > 50) bMoraleCategory = min(bMoraleCategory, MORALE_CONFIDENT);*/ // limit AI morale depending on morale and shock level bMoraleCategory = min(bMoraleCategory, max(MORALE_WORRIED, ((pSoldier->aiData.bOrders == SEEKENEMY ? pSoldier->aiData.bMorale + 20 : pSoldier->aiData.bMorale) * 100 / (100 + pSoldier->ShockLevelPercent())) / 20)); // prevent hopeless morale when not under attack if (bMoraleCategory == MORALE_HOPELESS && !pSoldier->aiData.bUnderFire) { bMoraleCategory = MORALE_WORRIED; } // check limits bMoraleCategory = max(bMoraleCategory, MORALE_HOPELESS); bMoraleCategory = min(bMoraleCategory, MORALE_FEARLESS); return(bMoraleCategory); } BOOLEAN AICheckSpecialRole(SOLDIERTYPE *pSoldier) { if (AICheckIsSniper(pSoldier) || AICheckIsMachinegunner(pSoldier) || AICheckIsMortarOperator(pSoldier) || AICheckIsRadioOperator(pSoldier) || AICheckIsCommander(pSoldier)) return TRUE; return FALSE; } BOOLEAN WeAttack(INT8 bTeam) { if (bTeam >= MAXTEAMS) { return FALSE; } if (bTeam != ENEMY_TEAM) { return FALSE; } // check that every soldier has SEEKENEMY order SOLDIERTYPE * pFriend; // Run through each friendly. for ( SoldierID iCounter = gTacticalStatus.Team[bTeam].bFirstID; iCounter <= gTacticalStatus.Team[bTeam].bLastID; ++iCounter ) { pFriend = iCounter; if (pFriend && pFriend->bActive && pFriend->stats.bLife >= OKLIFE && pFriend->aiData.bOrders != SEEKENEMY) { return FALSE; } } return TRUE; } UINT8 CountNearbyFriendsLastAttackHit( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance ) { CHECKF(pSoldier); SOLDIERTYPE * pFriend; UINT8 ubFriendCount = 0; // Run through each friendly. for ( SoldierID iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; ++iCounter ) { pFriend = iCounter; if (pFriend != pSoldier && pFriend->bActive && pFriend->stats.bLife >= OKLIFE && pFriend->aiData.bOrders > ONGUARD && pFriend->aiData.bOrders != SNIPER && PythSpacesAway( sGridNo, pFriend->sGridNo ) <= ubDistance && pFriend->aiData.bLastAttackHit ) { ubFriendCount++; } } return ubFriendCount; } UINT8 CountFriendsFlankSameSpot(SOLDIERTYPE *pSoldier, INT32 sSpot) { CHECKF(pSoldier); SOLDIERTYPE * pFriend; UINT8 ubFriendCount = 0; UINT8 ubFlankLeft = 0; UINT8 ubFlankRight = 0; if (TileIsOutOfBounds(sSpot)) { sSpot = ClosestKnownOpponent(pSoldier, NULL, NULL); } if (TileIsOutOfBounds(sSpot)) { return 0; } // 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 && pFriend->aiData.bAlertStatus == STATUS_RED && pFriend->aiData.bOrders > ONGUARD && pFriend->aiData.bOrders != SNIPER) { // check if this friend flanks around the same spot if (pFriend->numFlanks > 0 && pFriend->numFlanks < MAX_FLANKS_RED && PythSpacesAway(pFriend->lastFlankSpot, sSpot) < VISION_RANGE / 2) { if (pFriend->flags.lastFlankLeft) { ubFlankLeft++; } else { ubFlankRight++; } } } } return ubFlankLeft + ubFlankRight; } // check that soldier is flanking BOOLEAN AICheckIsFlanking( SOLDIERTYPE *pSoldier ) { CHECKF(pSoldier); if( pSoldier->aiData.bAlertStatus < STATUS_YELLOW || pSoldier->numFlanks == 0 || pSoldier->numFlanks >= MAX_FLANKS_RED ) { return FALSE; } return TRUE; } // sevenfm: determine minimum flanking directions to stop flanking depending on soldier's attitude UINT8 MinFlankDirections( SOLDIERTYPE *pSoldier ) { CHECKF(pSoldier); switch(pSoldier->aiData.bAttitude) { case CUNNINGAID: case CUNNINGSOLO: return 4; } return 2; } // count mobile friends that are in BLACK state and not in a dangerous place or have 3/4 APs or hit enemy recently // this is mostly used to check if we can cross dangerous area (in light at night or fresh corpses) UINT8 CountFriendsBlack( SOLDIERTYPE *pSoldier, INT32 sClosestOpponent ) { CHECKF(pSoldier); SOLDIERTYPE * pFriend; UINT8 ubFriendCount = 0; INT32 sFriendClosestOpponent; // by default, use closest known opponent if( sClosestOpponent == NOWHERE ) { sClosestOpponent = ClosestKnownOpponent( pSoldier, NULL, NULL ); } if(TileIsOutOfBounds(sClosestOpponent)) { return 0; } // Run through each friendly. for ( SoldierID iCounter = gTacticalStatus.Team[ pSoldier->bTeam ].bFirstID ; iCounter <= gTacticalStatus.Team[ pSoldier->bTeam ].bLastID ; ++iCounter ) { pFriend = iCounter; // Make sure that character is alive, not too shocked, and conscious if (pFriend != pSoldier && pFriend->bActive && pFriend->stats.bLife >= OKLIFE) { //sFriendClosestOpponent = ClosestKnownOpponent( pFriend, NULL, NULL ); sFriendClosestOpponent = ClosestSeenOpponent( pFriend, NULL, NULL ); if(!TileIsOutOfBounds(sFriendClosestOpponent) && PythSpacesAway( sClosestOpponent, sFriendClosestOpponent ) < (INT16)TACTICAL_RANGE / 4 && pFriend->aiData.bAlertStatus == STATUS_BLACK && pFriend->stats.bLife > pFriend->stats.bLifeMax / 2 && ( GetNearestRottingCorpseAIWarning( pFriend->sGridNo ) == 0 && !InLightAtNight(pFriend->sGridNo, pFriend->pathing.bLevel) || pFriend->bActionPoints > 3*pFriend->bInitialActionPoints/4 || pFriend->aiData.bLastAttackHit ) ) { ubFriendCount++; } } } return ubFriendCount; } // count friends under fire or with shock UINT16 CountTeamUnderAttack(INT8 bTeam, INT32 sGridNo, INT16 sDistance) { SOLDIERTYPE * pFriend; UINT16 ubFriendCount = 0; // safety check if (bTeam >= MAXTEAMS) return 0; // Run through each friendly. for ( SoldierID iCounter = gTacticalStatus.Team[bTeam].bFirstID; iCounter <= gTacticalStatus.Team[bTeam].bLastID; ++iCounter ) { pFriend = iCounter; if (pFriend && pFriend->bActive && pFriend->stats.bLife >= OKLIFE && PythSpacesAway(sGridNo, pFriend->sGridNo) <= sDistance && (pFriend->aiData.bUnderFire || pFriend->aiData.bShock > 0)) { ubFriendCount++; } } return ubFriendCount; } // check if we have a prone sight cover from known enemies at spot BOOLEAN ProneSightCoverAtSpot(SOLDIERTYPE *pSoldier, INT32 sSpot, BOOLEAN fUnlimited) { CHECKF(pSoldier); UINT32 uiLoop; SOLDIERTYPE *pOpponent; INT32 *pusLastLoc; INT8 *pbPersOL; INT8 *pbPublOL; INT8 *pbLastLevel; INT32 sThreatLoc; //INT32 iThreatCertainty; INT8 iThreatLevel; INT32 iDistanceVisible; // look through all opponents for those we know of for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { pOpponent = MercSlots[ uiLoop ]; // if this merc is inactive, at base, on assignment, dead, unconscious if (!pOpponent || pOpponent->stats.bLife < OKLIFE) { continue; // next merc } // if this man is neutral / on the same side, he's not an opponent if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide)) { continue; // next merc } pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID; pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID; pusLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpponent->ubID; pbLastLevel = gbLastKnownOppLevel[pSoldier->ubID] + pOpponent->ubID; // if this opponent is unknown personally and publicly if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN)) { continue; // next merc } // if personal knowledge is more up to date or at least equal if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) || (*pbPersOL == *pbPublOL)) { // using personal knowledge, obtain opponent's "best guess" gridno sThreatLoc = *pusLastLoc; iThreatLevel = *pbLastLevel; //iThreatCertainty = ThreatPercent[*pbPersOL - OLDEST_HEARD_VALUE]; } else { // using public knowledge, obtain opponent's "best guess" gridno sThreatLoc = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID]; iThreatLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpponent->ubID]; //iThreatCertainty = ThreatPercent[*pbPublOL - OLDEST_HEARD_VALUE]; } if (TileIsOutOfBounds(sThreatLoc)) { continue; } if (fUnlimited) { iDistanceVisible = NO_DISTANCE_LIMIT; } else { gbForceWeaponReady = true; iDistanceVisible = DistanceVisible(pSoldier, DIRECTION_IRRELEVANT, DIRECTION_IRRELEVANT, sSpot, pSoldier->pathing.bLevel, pSoldier->IsCowering(), GetPercentTunnelVision(pSoldier)); gbForceWeaponReady = false; //iDistanceVisible = pSoldier->GetMaxDistanceVisible(sSpot, pSoldier->pathing.bLevel, CALC_FROM_WANTED_DIR); } if (LocationToLocationLineOfSightTest(sThreatLoc, iThreatLevel, sSpot, pSoldier->pathing.bLevel, TRUE, iDistanceVisible, STANDING_LOS_POS, PRONE_LOS_POS)) { return FALSE; } } return TRUE; } // check if we have a sight cover from known enemies at spot BOOLEAN SightCoverAtSpot(SOLDIERTYPE *pSoldier, INT32 sSpot, BOOLEAN fUnlimited) { CHECKF(pSoldier); UINT32 uiLoop; SOLDIERTYPE *pOpponent; INT32 *pusLastLoc; INT8 *pbPersOL; INT8 *pbPublOL; INT8 *pbLastLevel; INT32 sThreatLoc; //INT32 iThreatCertainty; INT8 iThreatLevel; INT32 iDistanceVisible; // look through all opponents for those we know of for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { pOpponent = MercSlots[ uiLoop ]; // if this merc is inactive, at base, on assignment, dead, unconscious if (!pOpponent || pOpponent->stats.bLife < OKLIFE) { continue; // next merc } // if this man is neutral / on the same side, he's not an opponent if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide)) { continue; // next merc } pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID; pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID; pusLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpponent->ubID; pbLastLevel = gbLastKnownOppLevel[pSoldier->ubID] + pOpponent->ubID; // if this opponent is unknown personally and publicly if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN)) { continue; // next merc } // if personal knowledge is more up to date or at least equal if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) || (*pbPersOL == *pbPublOL)) { // using personal knowledge, obtain opponent's "best guess" gridno sThreatLoc = *pusLastLoc; iThreatLevel = *pbLastLevel; //iThreatCertainty = ThreatPercent[*pbPersOL - OLDEST_HEARD_VALUE]; } else { // using public knowledge, obtain opponent's "best guess" gridno sThreatLoc = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID]; iThreatLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpponent->ubID]; //iThreatCertainty = ThreatPercent[*pbPublOL - OLDEST_HEARD_VALUE]; } if (TileIsOutOfBounds(sThreatLoc)) { continue; } if(fUnlimited) { iDistanceVisible = NO_DISTANCE_LIMIT; } else { gbForceWeaponReady = true; iDistanceVisible = DistanceVisible(pSoldier, DIRECTION_IRRELEVANT, DIRECTION_IRRELEVANT, sSpot, pSoldier->pathing.bLevel, pSoldier->IsCowering(), GetPercentTunnelVision(pSoldier)); gbForceWeaponReady = false; //iDistanceVisible = pSoldier->GetMaxDistanceVisible(sSpot, pSoldier->pathing.bLevel, CALC_FROM_WANTED_DIR); } if (LocationToLocationLineOfSightTest(sThreatLoc, iThreatLevel, sSpot, pSoldier->pathing.bLevel, TRUE, iDistanceVisible)) { return FALSE; } } return TRUE; } BOOLEAN EnemySeenSoldierRecently( SOLDIERTYPE *pSoldier, UINT8 ubMax ) { UINT32 uiLoop; SOLDIERTYPE *pOpponent; //loop through all the enemies and determine the cover for ( uiLoop = 0; uiLoopstats.bLife < OKLIFE ) { continue; // next merc } // if this man is neutral / on the same side, he's not an opponent if ( CONSIDERED_NEUTRAL( pSoldier, pOpponent ) || (pSoldier->bSide == pOpponent->bSide) ) { continue; // next merc } // sevenfm: ignore empty vehicles if ( pOpponent->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle( pOpponent->bVehicleID ) == 0 ) { continue; } // if opponent is collapsed/breath collapsed if ( pOpponent->bCollapsed || pOpponent->bBreathCollapsed ) { continue; } // check that this opponent sees us if ( pOpponent->aiData.bOppList[pSoldier->ubID] >= SEEN_CURRENTLY && pOpponent->aiData.bOppList[pSoldier->ubID] <= ubMax ) { return(TRUE); } } return FALSE; } UINT16 CountTeamSeeSoldier( INT8 bTeam, SOLDIERTYPE *pSoldier ) { SOLDIERTYPE *pFriend; UINT16 ubFriends = 0; CHECKF( pSoldier ); if ( bTeam >= MAXTEAMS ) return 0; for ( SoldierID cnt = gTacticalStatus.Team[bTeam].bFirstID; cnt <= gTacticalStatus.Team[bTeam].bLastID; ++cnt ) { pFriend = cnt; if ( pFriend->bActive && pFriend->bInSector && pFriend->stats.bLife >= OKLIFE && !pFriend->bCollapsed && !pFriend->bBreathCollapsed ) { if ( pFriend->aiData.bOppList[pSoldier->ubID] == SEEN_CURRENTLY || pFriend->aiData.bOppList[pSoldier->ubID] == SEEN_THIS_TURN ) { ++ubFriends; } } } return ubFriends; } BOOLEAN CheckDoorAtGridno(UINT32 usGridNo) { STRUCTURE *pStructure; pStructure = FindStructure(usGridNo, STRUCTURE_ANYDOOR); if (pStructure != NULL) { return TRUE; } return FALSE; } BOOLEAN CheckDoorNearGridno(UINT32 usGridNo) { UINT8 ubMovementCost; INT32 sTempGridNo; UINT8 ubDirection; if (CheckDoorAtGridno(usGridNo)) { return TRUE; } for (ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++) { sTempGridNo = NewGridNo(usGridNo, DirectionInc(ubDirection)); if (sTempGridNo != usGridNo) { ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][0]; if (IS_TRAVELCOST_DOOR(ubMovementCost)) { return TRUE; } } } return FALSE; } BOOLEAN FindBombNearby( SOLDIERTYPE *pSoldier, INT32 sGridNo, UINT8 ubDistance ) { UINT32 uiBombIndex; INT32 sCheckGridno; INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXOffset, sYOffset; // determine maximum horizontal limits sMaxLeft = min( ubDistance, (sGridNo % MAXCOL)); sMaxRight = min( ubDistance, MAXCOL - ((sGridNo % MAXCOL) + 1)); // determine maximum vertical limits sMaxUp = min( ubDistance, (sGridNo / MAXROW)); sMaxDown = min( ubDistance, MAXROW - ((sGridNo / MAXROW) + 1)); for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; ++sYOffset) { for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; ++sXOffset) { sCheckGridno = sGridNo + sXOffset + (MAXCOL * sYOffset); if( TileIsOutOfBounds(sCheckGridno) ) { continue; } // search all bombs that we can see for (uiBombIndex = 0; uiBombIndex < guiNumWorldBombs; ++uiBombIndex) { if (gWorldBombs[ uiBombIndex ].fExists && gWorldItems[ gWorldBombs[ uiBombIndex ].iItemIndex ].sGridNo == sCheckGridno && gWorldItems[ gWorldBombs[ uiBombIndex ].iItemIndex ].ubLevel == pSoldier->pathing.bLevel && gWorldItems[ gWorldBombs[ uiBombIndex ].iItemIndex ].bVisible == VISIBLE && gWorldItems[ gWorldBombs[ uiBombIndex ].iItemIndex ].usFlags & WORLD_ITEM_ARMED_BOMB ) { return TRUE; } } } } return FALSE; } // danger percent based on distance to closest smoke effect UINT8 RedSmokeDanger(INT32 sGridNo, INT8 bLevel) { UINT32 uiCnt; INT32 sDist; INT32 sClosestDist; INT32 sMaxDist = min(gSkillTraitValues.usVOMortarRadius, TACTICAL_RANGE); INT32 sClosestSmoke = NOWHERE; UINT8 ubDangerPercent = 0; if (TileIsOutOfBounds(sGridNo)) { return 0; } if (!gSkillTraitValues.fROAllowArtillery) { return 0; } // no artillery strike danger underground if (gbWorldSectorZ > 0) { return 0; } // check if artillery strike was ordered by any team if (!CheckArtilleryStrike()) { return 0; } // no danger when in a building if (bLevel == 0 && CheckRoof(sGridNo)) { return 0; } // deep water should be safe if (DeepWater(sGridNo, bLevel)) { return 0; } // no danger when in dense terrain if (bLevel == 0 && TerrainDensity(sGridNo, bLevel, 2, FALSE) >= 20) { return 0; } //loop through all red smoke effects and find closest for (uiCnt = 0; uiCnt < guiNumSmokeEffects; uiCnt++) { if (gSmokeEffectData[uiCnt].fAllocated && gSmokeEffectData[uiCnt].bType == SIGNAL_SMOKE_EFFECT && !TileIsOutOfBounds(gSmokeEffectData[uiCnt].sGridNo)) { sDist = PythSpacesAway(gSmokeEffectData[uiCnt].sGridNo, sGridNo); if (sClosestSmoke == NOWHERE || sDist < sClosestDist) { sClosestDist = sDist; sClosestSmoke = gSmokeEffectData[uiCnt].sGridNo; } } } // if we found red smoke, calculate danger percent based on distance // 0% at DAY_VISION_RANGE/2, 100% at zero range if (sClosestSmoke != NOWHERE) { ubDangerPercent = 100 * (sMaxDist - min(sMaxDist, sClosestDist)) / sMaxDist; } return ubDangerPercent; } // check if artillery strike was ordered by any team BOOLEAN CheckArtilleryStrike(void) { UINT32 uiBombIndex; OBJECTTYPE *pObj; // search all bombs for (uiBombIndex = 0; uiBombIndex < guiNumWorldBombs; uiBombIndex++) { if (gWorldBombs[uiBombIndex].fExists && gWorldItems[gWorldBombs[uiBombIndex].iItemIndex].usFlags & WORLD_ITEM_ARMED_BOMB) { pObj = &(gWorldItems[gWorldBombs[uiBombIndex].iItemIndex].object); if (pObj && pObj->exists() && (*pObj)[0]->data.ubWireNetworkFlag & ANY_ARTILLERY_FLAG) { return TRUE; } } } return FALSE; } BOOLEAN CheckRoof(INT32 sGridNo) { if (FindStructure(sGridNo, STRUCTURE_ROOF) != NULL) { return TRUE; } return FALSE; } BOOLEAN FindNearbyExplosiveStructure(INT32 sSpot, INT8 bLevel) { INT32 sTempGridNo; UINT8 ubDirection; if (TileIsOutOfBounds(sSpot)) { return 0; } // check adjacent reachable tiles for (ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++) { sTempGridNo = NewGridNo(sSpot, DirectionInc(ubDirection)); if (sTempGridNo != sSpot) { if (FindStructFlag(sTempGridNo, bLevel, STRUCTURE_EXPLOSIVE)) { return TRUE; } } } return FALSE; } UINT8 TerrainDensity(INT32 sSpot, INT8 bLevel, UINT8 ubDistance, BOOLEAN fGrass) { if (TileIsOutOfBounds(sSpot)) return 0; INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXOffset, sYOffset; INT32 sCheckSpot = NOWHERE; INT32 sCountSpots = 0; INT32 sCountObstacles = 0; UINT16 usRoom1, usRoom2; STRUCTURE *pCurrent; INT16 sDesiredLevel; // determine maximum horizontal limits sMaxLeft = min(ubDistance, (sSpot % MAXCOL)); sMaxRight = min(ubDistance, MAXCOL - ((sSpot % MAXCOL) + 1)); // determine maximum vertical limits sMaxUp = min(ubDistance, (sSpot / MAXROW)); sMaxDown = min(ubDistance, MAXROW - ((sSpot / MAXROW) + 1)); // count obstacles for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++) { for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++) { sCheckSpot = sSpot + sXOffset + (MAXCOL * sYOffset); if (TileIsOutOfBounds(sCheckSpot)) { continue; } if (InARoom(sSpot, &usRoom1) != InARoom(sCheckSpot, &usRoom2) || usRoom1 != usRoom2) { continue; } sCountSpots++; if (!IsLocationSittableExcludingPeople(sCheckSpot, bLevel)) { sCountObstacles++; } if (fGrass && IsLocationSittableExcludingPeople(sCheckSpot, bLevel)) { pCurrent = gpWorldLevelData[sCheckSpot].pStructureHead; if (bLevel > 0) sDesiredLevel = STRUCTURE_ON_ROOF; else sDesiredLevel = STRUCTURE_ON_GROUND; if (pCurrent != NULL && pCurrent->sCubeOffset == sDesiredLevel && pCurrent->pDBStructureRef->pDBStructure->ubArmour == 4) // light vegetation { sCountObstacles++; } } } } if (sCountSpots > 0) return 100 * sCountObstacles / sCountSpots; else return 0; } INT16 DistanceToClosestActiveOpponent(SOLDIERTYPE *pSoldier, INT32 sSpot) { INT32 sGridNo; UINT32 uiLoop; INT8 bLevel; SOLDIERTYPE *pOpponent; INT16 sDistance, sClosestDistance = -1; if (!pSoldier || TileIsOutOfBounds(sSpot)) { return 0; } // look through this man's personal & public opplists for opponents known for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { pOpponent = MercSlots[uiLoop]; // if this merc is inactive, at base, on assignment, or dead if (!pOpponent) { continue; // next merc } if (!ValidOpponent(pSoldier, pOpponent)) { continue; } if (pOpponent->stats.bLife < OKLIFE) { continue; } // if this opponent is unknown personally and publicly if (pSoldier->aiData.bOppList[pOpponent->ubID] == NOT_HEARD_OR_SEEN) { continue; } // obtain opponent's location and level sGridNo = gsLastKnownOppLoc[pSoldier->ubID][pOpponent->ubID]; bLevel = gbLastKnownOppLevel[pSoldier->ubID][pOpponent->ubID]; if (TileIsOutOfBounds(sGridNo)) { continue; } sDistance = PythSpacesAway(sSpot, sGridNo); if (sClosestDistance < 0 || sDistance < sClosestDistance) { sClosestDistance = sDistance; } } return sClosestDistance; } BOOLEAN ValidOpponent(SOLDIERTYPE* pSoldier, SOLDIERTYPE* pOpponent) { if (!pSoldier || !pOpponent) { return FALSE; } if (!pOpponent->bActive || !pOpponent->bInSector || pOpponent->stats.bLife == 0 || CONSIDERED_NEUTRAL(pSoldier, pOpponent) || pSoldier->bSide == pOpponent->bSide || pSoldier->aiData.bAttitude == ATTACKSLAYONLY && pOpponent->ubProfile != SLAY || (pOpponent->ubWhatKindOfMercAmI == MERC_TYPE__VEHICLE && GetNumberInVehicle(pOpponent->bVehicleID) == 0) || gTacticalStatus.bBoxingState == BOXING && BOXER(pSoldier) && !BOXER(pOpponent) || pOpponent->ubBodyType == CROW) { return FALSE; } return TRUE; } BOOLEAN AnyCoverFromSpot( INT32 sSpot, INT8 bLevel, INT32 sThreatLoc, INT8 bThreatLevel ) { UINT8 ubDirection; INT32 sCoverSpot; INT8 bCoverHeight; if( TileIsOutOfBounds( sSpot ) || TileIsOutOfBounds(sThreatLoc) ) { return FALSE; } ubDirection = GetDirectionFromCenterCellXYGridNo(sSpot, sThreatLoc); sCoverSpot = NewGridNo( sSpot, DirectionInc( ubDirection ) ); if ( TileIsOutOfBounds( sCoverSpot ) ) { return FALSE; } if ( WhoIsThere2( sCoverSpot, bLevel ) != NOBODY ) { return FALSE; } if ( IsLocationSittable( sCoverSpot, bLevel ) ) { return FALSE; } bCoverHeight = GetTallestStructureHeight( sCoverSpot, bLevel ); if ( bCoverHeight >= 2 ) { return TRUE; } return FALSE; } UINT16 CountSeenEnemiesLastTurn( SOLDIERTYPE* pSoldier ) { CHECKF(pSoldier); UINT8 ubTeamLoop; UINT16 cnt = 0; for( ubTeamLoop = 0; ubTeamLoop < MAXTEAMS; ubTeamLoop++ ) { if( !gTacticalStatus.Team[ubTeamLoop].bTeamActive ) continue; if( gTacticalStatus.Team[ ubTeamLoop ].bSide != pSoldier->bSide ) { // consider guys in this team, which isn't on our side for( SoldierID ubIDLoop = gTacticalStatus.Team[ ubTeamLoop ].bFirstID; ubIDLoop <= gTacticalStatus.Team[ ubTeamLoop ].bLastID; ++ubIDLoop ) { // if this guy SAW an enemy recently... if( pSoldier->aiData.bOppList[ ubIDLoop ] >= SEEN_CURRENTLY && pSoldier->aiData.bOppList[ ubIDLoop ] <= SEEN_LAST_TURN ) { cnt++; } } } } return cnt; } BOOLEAN NorthSpot(INT32 sSpot, INT8 bLevel) { if (TileIsOutOfBounds(sSpot)) return FALSE; if (bLevel > 0) return FALSE; INT32 sNextSpot = NewGridNo(sSpot, DirectionInc(NORTHWEST)); if (gubWorldMovementCosts[sSpot + DirectionInc(NORTHWEST)][NORTHWEST][0] == TRAVELCOST_OFF_MAP || gubWorldMovementCosts[sNextSpot + DirectionInc(NORTHWEST)][NORTHWEST][0] == TRAVELCOST_OFF_MAP) { return TRUE; } return FALSE; } // use soldier AI - merc bodytype, no robots/tanks/boxers/etc BOOLEAN SoldierAI(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); BOOLEAN fCivilian = (PTR_CIVILIAN && (pSoldier->ubCivilianGroup == NON_CIV_GROUP || (pSoldier->aiData.bNeutral && gTacticalStatus.fCivGroupHostile[pSoldier->ubCivilianGroup] == CIV_GROUP_NEUTRAL) || (pSoldier->ubBodyType >= FATCIV && pSoldier->ubBodyType <= CRIPPLECIV))); if (!IS_MERC_BODY_TYPE(pSoldier) || pSoldier->aiData.bNeutral || fCivilian || BOXER(pSoldier) || ARMED_VEHICLE(pSoldier) || pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE || AM_A_ROBOT(pSoldier) || ENEMYROBOT(pSoldier)) return FALSE; return TRUE; } UINT8 SpotDangerLevel(SOLDIERTYPE *pSoldier, INT32 sGridNo) { if (!pSoldier) return 0; if (TileIsOutOfBounds(sGridNo)) return 0; BOOLEAN fAlerted = FALSE; BOOLEAN fGreen = FALSE; BOOLEAN fProfile = FALSE; BOOLEAN fNeutral = FALSE; UINT8 ubLevel = 0; if (pSoldier->aiData.bAlertStatus < STATUS_YELLOW) fGreen = TRUE; if (pSoldier->ubProfile != NO_PROFILE) fProfile = TRUE; if (pSoldier->aiData.bNeutral) fNeutral = TRUE; if ((pSoldier->aiData.bAlertStatus >= STATUS_RED || pSoldier->ubSoldierClass == SOLDIER_CLASS_ELITE)) fAlerted = TRUE; if (!fProfile && !fNeutral && !gGameExternalOptions.fAITacticalRetreat && NorthSpot(sGridNo, pSoldier->pathing.bLevel) || !fProfile && fGreen && CheckDoorNearGridno(sGridNo) || Water(sGridNo, pSoldier->pathing.bLevel) && !pSoldier->IsFlanking() || CorpseWarning(pSoldier, sGridNo, pSoldier->pathing.bLevel)) ubLevel = 1; if (fAlerted && !fNeutral && (InLightAtNight(sGridNo, pSoldier->pathing.bLevel) || FindNearbyExplosiveStructure(sGridNo, pSoldier->pathing.bLevel))) ubLevel = 2; if (DeepWater(sGridNo, pSoldier->pathing.bLevel) && !pSoldier->IsFlanking() || RedSmokeDanger(sGridNo, pSoldier->pathing.bLevel)) ubLevel = 3; if (InGas(pSoldier, sGridNo) || FindBombNearby(pSoldier, sGridNo, BOMB_DETECTION_RANGE)) ubLevel = 4; return ubLevel; } BOOLEAN CheckNPCDestination(SOLDIERTYPE *pSoldier, INT32 sGridNo) { if (!pSoldier) return FALSE; if (TileIsOutOfBounds(sGridNo)) return FALSE; // find current danger level UINT8 ubLevel = SpotDangerLevel(pSoldier, pSoldier->sGridNo); // find danger level at target spot UINT8 ubTargetLevel = SpotDangerLevel(pSoldier, sGridNo); // avoid moving into dangerous spot if (ubTargetLevel > 0 && ubTargetLevel >= ubLevel) return FALSE; return TRUE; } BOOLEAN AllowDeepWaterFlanking(SOLDIERTYPE *pSoldier) { if (SoldierAI(pSoldier) && pSoldier->bTeam == ENEMY_TEAM && pSoldier->aiData.bOrders == SEEKENEMY && (pSoldier->aiData.bAttitude == CUNNINGSOLO || gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSoldier, ATHLETICS_NT)) && pSoldier->aiData.bAlertStatus >= STATUS_RED && !pSoldier->aiData.bUnderFire && !GuySawEnemy(pSoldier)) { return TRUE; } return FALSE; } UINT8 AIDirection(INT32 sSpot1, INT32 sSpot2) { if (TileIsOutOfBounds(sSpot1) || TileIsOutOfBounds(sSpot2)) { return DIRECTION_IRRELEVANT; } return GetDirectionFromCenterCellXYGridNo(sSpot1, sSpot2); } BOOLEAN AICheckIsSniper(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); if (!SoldierAI(pSoldier)) { return FALSE; } /*if( AIGunType(pSoldier) != GUN_SN_RIFLE ) { return FALSE; }*/ if ((AIGunRange(pSoldier) > TACTICAL_RANGE || AICheckHasWeaponOfType(pSoldier, GUN_SN_RIFLE)) && AIGunScoped(pSoldier) && pSoldier->stats.bMarksmanship > 90 && gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSoldier, SNIPER_NT)) { return TRUE; } return FALSE; } BOOLEAN AICheckIsMarksman(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); if (!SoldierAI(pSoldier)) { return FALSE; } if (!AICheckHasGun(pSoldier)) { return FALSE; } if (AIGunType(pSoldier) != GUN_SN_RIFLE && AIGunType(pSoldier) != GUN_RIFLE && AIGunType(pSoldier) != GUN_AS_RIFLE) { return FALSE; } if (AIGunRange(pSoldier) >= TACTICAL_RANGE && (AIGunScoped(pSoldier) || pSoldier->stats.bMarksmanship > 90 || gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSoldier, SNIPER_NT))) { return TRUE; } return FALSE; } BOOLEAN AICheckIsRadioOperator(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); if (!SoldierAI(pSoldier)) { return FALSE; } if (pSoldier->CanUseRadio(FALSE)) return TRUE; return FALSE; } BOOLEAN AICheckIsMedic(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); if (!SoldierAI(pSoldier)) { return FALSE; } if (pSoldier->CanMedicAI() && gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSoldier, DOCTOR_NT)) { return TRUE; } /*if( !HAS_SKILL_TRAIT( pSoldier, DOCTOR_NT) ) { return FALSE; } if( FindFirstAidKit( pSoldier ) != NO_SLOT || FindMedKit( pSoldier ) != NO_SLOT ) { return TRUE; }*/ return FALSE; } BOOLEAN AICheckIsMortarOperator(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); if (!SoldierAI(pSoldier)) { return FALSE; } if (pSoldier->HasMortar()) { return TRUE; } return FALSE; } BOOLEAN AICheckIsGLOperator(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); if (!SoldierAI(pSoldier)) { return FALSE; } // find GL INT8 bWeaponIn = FindAIUsableObjClass(pSoldier, IC_LAUNCHER); if (bWeaponIn != NO_SLOT && (EnoughAmmo(pSoldier, FALSE, bWeaponIn) || FindAmmoToReload(pSoldier, bWeaponIn, NO_SLOT) != NO_SLOT)) { return TRUE; } // check for attached GL INT8 bGunSlot = FindAIUsableObjClass(pSoldier, IC_GUN); INT8 bRealWeaponMode = pSoldier->bWeaponMode; pSoldier->bWeaponMode = WM_ATTACHED_GL; // So that EnoughAmmo will check for a grenade not a bullet if (bGunSlot != NO_SLOT && IsGrenadeLauncherAttached(&pSoldier->inv[bGunSlot]) && (EnoughAmmo(pSoldier, FALSE, bGunSlot) || FindAmmoToReload(pSoldier, bGunSlot, NO_SLOT) != NO_SLOT)) { pSoldier->bWeaponMode = bRealWeaponMode; return TRUE; } pSoldier->bWeaponMode = bRealWeaponMode; return FALSE; } BOOLEAN AICheckIsOfficer(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); if (!SoldierAI(pSoldier)) { return FALSE; } if (gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSoldier, SQUADLEADER_NT)) { return TRUE; } return FALSE; } BOOLEAN AICheckIsCommander(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); if (!SoldierAI(pSoldier)) { return FALSE; } if (gGameOptions.fNewTraitSystem && NUM_SKILL_TRAITS(pSoldier, SQUADLEADER_NT) > 1) { return TRUE; } return FALSE; } BOOLEAN AICheckIsMachinegunner(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); if (!SoldierAI(pSoldier)) { return FALSE; } if (AIGunType(pSoldier) == GUN_LMG) { return TRUE; } return FALSE; } BOOLEAN AIGunInHandScoped(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); if (UsingNewCTHSystem() == false && IsScoped(&pSoldier->inv[HANDPOS])) { return TRUE; } if (UsingNewCTHSystem() == true && NCTHIsScoped(&pSoldier->inv[HANDPOS])) { return TRUE; } return FALSE; } // check if gun that AI can use is scoped BOOLEAN AIGunScoped(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); INT8 bWeaponIn; bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN); if (bWeaponIn == NO_SLOT) { return FALSE; } if (pSoldier->inv[bWeaponIn].exists()) { if (UsingNewCTHSystem()) { return NCTHIsScoped(&pSoldier->inv[bWeaponIn]); } else { return IsScoped(&pSoldier->inv[bWeaponIn]); } } return FALSE; } // return range for the AI gun in tiles UINT16 AIGunRange(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); INT8 bWeaponIn; bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN); if (bWeaponIn == NO_SLOT) { return 0; } if (pSoldier->inv[bWeaponIn].exists()) { return GunRange(&pSoldier->inv[bWeaponIn], pSoldier) / CELL_X_SIZE; } return 0; } UINT16 AIGunClass(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); INT8 bWeaponIn; bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN); if (bWeaponIn == NO_SLOT) { return 0; } if (pSoldier->inv[bWeaponIn].exists()) { return Weapon[pSoldier->inv[bWeaponIn].usItem].ubWeaponClass; } return 0; } UINT16 AIGunType(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); INT8 bWeaponIn; bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN); if (bWeaponIn == NO_SLOT) { return 0; } if (pSoldier->inv[bWeaponIn].exists()) { return Weapon[pSoldier->inv[bWeaponIn].usItem].ubWeaponType; } return 0; } UINT8 AIGunDeadliness(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); INT8 bWeaponIn; bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN); if (bWeaponIn == NO_SLOT) { return 0; } if (pSoldier->inv[bWeaponIn].exists()) { return Weapon[pSoldier->inv[bWeaponIn].usItem].ubDeadliness; } return 0; } UINT16 AIGunAmmo(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); INT8 bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN); if (bWeaponIn == NO_SLOT) { return 0; } if (pSoldier->inv[bWeaponIn].exists()) { return pSoldier->inv[bWeaponIn][0]->data.gun.ubGunShotsLeft; } return 0; } BOOLEAN AIGunAutofireCapable(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); INT8 bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN); if (bWeaponIn == NO_SLOT) { return FALSE; } if (pSoldier->inv[bWeaponIn].exists()) { return IsGunAutofireCapable(&pSoldier->inv[bWeaponIn]); } return FALSE; } BOOLEAN FindObstacleNearSpot(INT32 sSpot, INT8 bLevel) { if (TileIsOutOfBounds(sSpot)) { return FALSE; } /*if( !IsLocationSittableExcludingPeople(sSpot, bLevel) ) { return FALSE; }*/ UINT8 ubMovementCost; INT32 sTempGridNo; UINT8 ubDirection; // check adjacent reachable tiles for (ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++) { sTempGridNo = NewGridNo(sSpot, DirectionInc(ubDirection)); if (sTempGridNo != sSpot) { ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][bLevel]; if (ubMovementCost >= TRAVELCOST_BLOCKED || !IsLocationSittableExcludingPeople(sTempGridNo, bLevel)) { return(TRUE); } } } return FALSE; } BOOLEAN InSmoke(INT32 sGridNo, INT8 bLevel) { if (TileIsOutOfBounds(sGridNo)) { return FALSE; } if (gpWorldLevelData[sGridNo].ubExtFlags[bLevel] & (MAPELEMENT_EXT_SMOKE)) { return TRUE; } return FALSE; } BOOLEAN InSmokeNearby(INT32 sGridNo, INT8 bLevel) { if (TileIsOutOfBounds(sGridNo)) { return FALSE; } if (gpWorldLevelData[sGridNo].ubExtFlags[bLevel] & (MAPELEMENT_EXT_SMOKE)) { return TRUE; } UINT8 ubDirection; INT32 sTempGridNo; UINT8 ubMovementCost; // check adjacent reachable tiles for (ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++) { sTempGridNo = NewGridNo(sGridNo, DirectionInc(ubDirection)); if (sTempGridNo != sGridNo) { ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][bLevel]; if (ubMovementCost < TRAVELCOST_BLOCKED && gpWorldLevelData[sTempGridNo].ubExtFlags[bLevel] & (MAPELEMENT_EXT_SMOKE)) { return(TRUE); } } } return FALSE; } BOOLEAN CorpseWarning(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bLevel) { CHECKF(pSoldier); INT32 cnt; ROTTING_CORPSE *pCorpse; UINT8 ubDistance = CORPSE_WARNING_DIST; for (cnt = 0; cnt < giNumRottingCorpse; ++cnt) { pCorpse = &(gRottingCorpse[cnt]); if (pCorpse && pCorpse->fActivated && pCorpse->def.ubType < ROTTING_STAGE2 && //pCorpse->def.ubType <= FMERC_FALLF && pCorpse->def.ubBodyType <= REGFEMALE && pCorpse->def.ubAIWarningValue > 0 && pCorpse->def.bLevel == bLevel && !TileIsOutOfBounds(pCorpse->def.sGridNo) && PythSpacesAway(sGridNo, pCorpse->def.sGridNo) <= ubDistance && (pSoldier->bTeam == ENEMY_TEAM && CorpseEnemyTeam(pCorpse) || pSoldier->bTeam == MILITIA_TEAM && CorpseMilitiaTeam(pCorpse) || pSoldier->bTeam == CIV_TEAM && !pSoldier->aiData.bNeutral)) { return TRUE; } } return FALSE; } BOOLEAN CorpseEnemyTeam(ROTTING_CORPSE *pCorpse) { CHECKF(pCorpse); // check whether corpse has soldier's uniform for (UINT8 i = UNIFORM_ENEMY_ADMIN; i <= UNIFORM_ENEMY_ELITE; ++i) { if (COMPARE_PALETTEREP_ID(pCorpse->def.VestPal, gUniformColors[i].vest) && COMPARE_PALETTEREP_ID(pCorpse->def.PantsPal, gUniformColors[i].pants)) { return TRUE; } } return FALSE; } BOOLEAN CorpseMilitiaTeam(ROTTING_CORPSE *pCorpse) { CHECKF(pCorpse); // check whether corpse has soldier's uniform for (UINT8 i = UNIFORM_MILITIA_ROOKIE; i <= UNIFORM_MILITIA_ELITE; ++i) { if (COMPARE_PALETTEREP_ID(pCorpse->def.VestPal, gUniformColors[i].vest) && COMPARE_PALETTEREP_ID(pCorpse->def.PantsPal, gUniformColors[i].pants)) { return TRUE; } } return FALSE; } // check that current loaded sector is underground BOOLEAN AICheckUnderground(void) { // not underground if (gbWorldSectorZ > 0) { return TRUE; } return FALSE; } // check if we have any sight cover from known enemies at spot BOOLEAN AnyCoverAtSpot(SOLDIERTYPE *pSoldier, INT32 sSpot) { CHECKF(pSoldier); UINT32 uiLoop; SOLDIERTYPE *pOpponent; INT32 *pusLastLoc; INT8 *pbPersOL; INT8 *pbPublOL; INT8 *pbLastLevel; INT32 sThreatLoc; //INT32 iThreatCertainty; INT8 iThreatLevel; // look through all opponents for those we know of for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { pOpponent = MercSlots[uiLoop]; // if this merc is inactive, at base, on assignment, dead, unconscious if (!pOpponent || pOpponent->stats.bLife < OKLIFE) { continue; // next merc } // if this man is neutral / on the same side, he's not an opponent if (CONSIDERED_NEUTRAL(pSoldier, pOpponent) || (pSoldier->bSide == pOpponent->bSide)) { continue; // next merc } pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID; pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID; pusLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpponent->ubID; pbLastLevel = gbLastKnownOppLevel[pSoldier->ubID] + pOpponent->ubID; // if this opponent is unknown personally and publicly if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN)) { continue; // next merc } // if personal knowledge is more up to date or at least equal // sevenfm: fix for unknown public location if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) || (*pbPersOL == *pbPublOL) || *pbPersOL != NOT_HEARD_OR_SEEN && TileIsOutOfBounds(gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID]) && !TileIsOutOfBounds(gsLastKnownOppLoc[pSoldier->ubID][pOpponent->ubID])) { // using personal knowledge, obtain opponent's "best guess" gridno sThreatLoc = *pusLastLoc; iThreatLevel = *pbLastLevel; //iThreatCertainty = ThreatPercent[*pbPersOL - OLDEST_HEARD_VALUE]; } else { // using public knowledge, obtain opponent's "best guess" gridno sThreatLoc = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID]; iThreatLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpponent->ubID]; //iThreatCertainty = ThreatPercent[*pbPublOL - OLDEST_HEARD_VALUE]; } if (!AnyCoverFromSpot(sSpot, pSoldier->pathing.bLevel, sThreatLoc, iThreatLevel) && LocationToLocationLineOfSightTest(sThreatLoc, iThreatLevel, sSpot, pSoldier->pathing.bLevel, TRUE, NO_DISTANCE_LIMIT, STANDING_LOS_POS, PRONE_LOS_POS)) { return FALSE; } } return TRUE; } BOOLEAN AICheckHasWeaponOfType(SOLDIERTYPE *pSoldier, UINT8 ubWeaponType) { CHECKF(pSoldier); INT8 invsize = (INT8)pSoldier->inv.size(); for (INT8 bLoop = 0; bLoop < invsize; ++bLoop) { if (pSoldier->inv[bLoop].exists() && Item[pSoldier->inv[bLoop].usItem].usItemClass == IC_GUN && Weapon[Item[pSoldier->inv[bLoop].usItem].ubClassIndex].ubWeaponType == ubWeaponType) { return TRUE; } } return FALSE; } BOOLEAN AICheckHasGun(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); if (FindAIUsableObjClass(pSoldier, IC_GUN) != NO_SLOT) { return TRUE; } return FALSE; } BOOLEAN AICheckShortWeaponRange(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); if (!AICheckHasGun(pSoldier)) { return TRUE; } if (AIGunRange(pSoldier) < TACTICAL_RANGE / 2) { return TRUE; } return FALSE; } BOOLEAN NightLight(void) { //if( GetTimeOfDayAmbientLightLevel() >= NORMAL_LIGHTLEVEL_DAY + 2 ) if (gubEnvLightValue >= NORMAL_LIGHTLEVEL_NIGHT - 3) { return TRUE; } return FALSE; } BOOLEAN DuskLight(void) { // average between day and night if (gubEnvLightValue >= (NORMAL_LIGHTLEVEL_NIGHT + NORMAL_LIGHTLEVEL_DAY) / 2) { return TRUE; } return FALSE; } BOOLEAN UsePersonalKnowledge(SOLDIERTYPE *pSoldier, SoldierID ubOpponentID) { INT8 bPersonalKnowledge; INT8 bPublicKnowledge; if (!pSoldier || ubOpponentID == NOBODY) { return FALSE; } bPersonalKnowledge = PersonalKnowledge(pSoldier, ubOpponentID); bPublicKnowledge = PublicKnowledge(pSoldier->bTeam, ubOpponentID); if (gubKnowledgeValue[bPublicKnowledge - OLDEST_HEARD_VALUE][bPersonalKnowledge - OLDEST_HEARD_VALUE] > 0 || bPersonalKnowledge != NOT_HEARD_OR_SEEN && TileIsOutOfBounds(KnownPublicLocation(pSoldier->bTeam, ubOpponentID)) && !TileIsOutOfBounds(KnownPersonalLocation(pSoldier, ubOpponentID))) { return TRUE; } return FALSE; } INT8 Knowledge(SOLDIERTYPE *pSoldier, SoldierID ubOpponentID) { if (!pSoldier || ubOpponentID == NOBODY) { return NOT_HEARD_OR_SEEN; } if (UsePersonalKnowledge(pSoldier, ubOpponentID)) { return PersonalKnowledge(pSoldier, ubOpponentID); } return PublicKnowledge(pSoldier->bTeam, ubOpponentID); } INT32 KnownLocation(SOLDIERTYPE *pSoldier, SoldierID ubOpponentID) { if (!pSoldier || ubOpponentID == NOBODY) { return NOWHERE; } if (UsePersonalKnowledge(pSoldier, ubOpponentID)) { return KnownPersonalLocation(pSoldier, ubOpponentID); } return KnownPublicLocation(pSoldier->bTeam, ubOpponentID); } INT8 KnownLevel(SOLDIERTYPE *pSoldier, SoldierID ubOpponentID) { if (!pSoldier || ubOpponentID == NOBODY) { return 0; } if (UsePersonalKnowledge(pSoldier, ubOpponentID)) { return KnownPersonalLevel(pSoldier, ubOpponentID); } return KnownPublicLevel(pSoldier->bTeam, ubOpponentID); } INT8 PersonalKnowledge(SOLDIERTYPE *pSoldier, SoldierID ubOpponentID) { if (!pSoldier || ubOpponentID == NOBODY) { return NOT_HEARD_OR_SEEN; } return pSoldier->aiData.bOppList[ubOpponentID]; } INT32 KnownPersonalLocation(SOLDIERTYPE *pSoldier, SoldierID ubOpponentID) { if (!pSoldier || ubOpponentID == NOBODY) { return NOWHERE; } /*if(PersonalKnowledge(pSoldier, ubOpponentID) == NOT_HEARD_OR_SEEN) { return NOWHERE; }*/ return gsLastKnownOppLoc[pSoldier->ubID][ubOpponentID]; } INT8 KnownPersonalLevel(SOLDIERTYPE *pSoldier, SoldierID ubOpponentID) { if (!pSoldier || ubOpponentID == NOBODY) { return 0; } return gbLastKnownOppLevel[pSoldier->ubID][ubOpponentID]; } INT8 PublicKnowledge(UINT8 bTeam, SoldierID ubOpponentID) { if (bTeam >= MAXTEAMS || ubOpponentID == NOBODY) { return NOT_HEARD_OR_SEEN; } return gbPublicOpplist[bTeam][ubOpponentID]; } INT32 KnownPublicLocation(UINT8 bTeam, SoldierID ubOpponentID) { if (bTeam >= MAXTEAMS || ubOpponentID == NOBODY) { return NOWHERE; } /*if (PublicKnowledge(bTeam, ubOpponentID) == NOT_HEARD_OR_SEEN) { return NOWHERE; }*/ return gsPublicLastKnownOppLoc[bTeam][ubOpponentID]; } INT8 KnownPublicLevel(UINT8 bTeam, SoldierID ubOpponentID) { if (bTeam >= MAXTEAMS || ubOpponentID == NOBODY) { return 0; } return gbPublicLastKnownOppLevel[bTeam][ubOpponentID]; } UINT8 ArmyPercentKilled(void) { if (gTacticalStatus.Team[ENEMY_TEAM].bMenInSector + gTacticalStatus.ubArmyGuysKilled == 0) { return 0; } return 100 * gTacticalStatus.ubArmyGuysKilled / (gTacticalStatus.Team[ENEMY_TEAM].bMenInSector + gTacticalStatus.ubArmyGuysKilled); } UINT8 TeamPercentKilled(INT8 bTeam) { if (bTeam == ENEMY_TEAM) { return ArmyPercentKilled(); } return 0; } BOOLEAN TeamHighPercentKilled(INT8 bTeam) { if (bTeam == ENEMY_TEAM && ArmyPercentKilled() > ArmyPercentKilledTolerance()) { return TRUE; } return FALSE; } // decide how many soldiers can be killed before alarm will be raised UINT8 ArmyPercentKilledTolerance(void) { // 50% at day, 25% at night, 25-33% for restricted sectors return 100 / (2 + SectorCurfew(TRUE)); } UINT8 SectorCurfew(BOOLEAN fNight) { UINT8 ubSectorId = SECTOR(gWorldSectorX, gWorldSectorY); UINT8 ubSectorData = 0; ubSectorData = SectorExternalData[ubSectorId][gbWorldSectorZ].usCurfewValue; if (fNight && NightLight()) // suspicious at night ubSectorData = max(ubSectorData, 1); if (gbWorldSectorZ > 0) // underground we are always suspicious ubSectorData = max(ubSectorData, 2); 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; } // first call PrepareThreatlist to make threat list SoldierID ClosestKnownThreatID(SOLDIERTYPE *pSoldier, UINT32 uiThreatCnt) { CHECKF(pSoldier); UINT32 uiLoop; INT32 sClosestOpponent = NOWHERE; INT32 iRange, iClosestRange; SoldierID ubClosestOpponentID = NOBODY; // use global defined threat list for (uiLoop = 0; uiLoop < uiThreatCnt; uiLoop++) { // if for some reason we have incorrect location if (TileIsOutOfBounds(Threat[uiLoop].sGridNo)) continue; iRange = GetRangeInCellCoordsFromGridNoDiff(pSoldier->sGridNo, Threat[uiLoop].sGridNo); if (ubClosestOpponentID == NOBODY || iRange < iClosestRange) { ubClosestOpponentID = Threat[uiLoop].pOpponent->ubID; iClosestRange = iRange; sClosestOpponent = Threat[uiLoop].sGridNo; } } return(ubClosestOpponentID); } // first call PrepareThreatlist to make threat list SoldierID ClosestSeenThreatID(SOLDIERTYPE *pSoldier, UINT32 uiThreatCnt, UINT8 ubMax) { CHECKF(pSoldier); UINT32 uiLoop; INT32 sClosestOpponent = NOWHERE; INT32 iRange, iClosestRange; SoldierID ubClosestOpponentID = NOBODY; // use global defined threat list for (uiLoop = 0; uiLoop < uiThreatCnt; uiLoop++) { // if for some reason we have incorrect location if (TileIsOutOfBounds(Threat[uiLoop].sGridNo)) continue; // check knowledge if (Threat[uiLoop].bPersonalKnowledge < SEEN_CURRENTLY || Threat[uiLoop].bPersonalKnowledge > ubMax) continue; iRange = GetRangeInCellCoordsFromGridNoDiff(pSoldier->sGridNo, Threat[uiLoop].sGridNo); if (ubClosestOpponentID == NOBODY || iRange < iClosestRange) { ubClosestOpponentID = Threat[uiLoop].pOpponent->ubID; iClosestRange = iRange; sClosestOpponent = Threat[uiLoop].sGridNo; } } return(ubClosestOpponentID); } UINT32 PrepareThreatlist(SOLDIERTYPE *pSoldier) { SOLDIERTYPE *pOpponent; INT32 iThreatRange, iClosestThreatRange = 1500; UINT32 uiLoop; INT8 bPersonalKnowledge; INT8 bPublicKnowledge; INT8 bKnowledge; INT32 sThreatLoc; INT8 bThreatLevel; INT32 iThreatCertainty; INT32 iMaxThreatRange = MAX_THREAT_RANGE + AI_PATHCOST_RADIUS; UINT32 uiThreatCnt = 0; if (!pSoldier) return 0; // look through all opponents for those we know of for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { pOpponent = MercSlots[uiLoop]; // if this merc is inactive, at base, on assignment, dead, unconscious if (!pOpponent || pOpponent->stats.bLife < OKLIFE) { continue; // next merc } if (!ValidOpponent(pSoldier, pOpponent)) { continue; } bKnowledge = Knowledge(pSoldier, pOpponent->ubID); bPersonalKnowledge = PersonalKnowledge(pSoldier, pOpponent->ubID); bPublicKnowledge = PublicKnowledge(pSoldier->bTeam, pOpponent->ubID); // if this opponent is unknown personally and publicly if (bKnowledge == NOT_HEARD_OR_SEEN) { continue; // next merc } sThreatLoc = KnownLocation(pSoldier, pOpponent->ubID); bThreatLevel = KnownLevel(pSoldier, pOpponent->ubID); iThreatCertainty = ThreatPercent[bKnowledge - OLDEST_HEARD_VALUE]; // safety check if (TileIsOutOfBounds(sThreatLoc)) { continue; } // calculate how far away this threat is (in adjusted pixels) iThreatRange = GetRangeInCellCoordsFromGridNoDiff(pSoldier->sGridNo, sThreatLoc); // if this opponent is believed to be too far away to really be a threat if (iThreatRange > iMaxThreatRange) { continue; // check next opponent } // remember this opponent as a current threat, but DON'T REDUCE FOR COVER! Threat[uiThreatCnt].iValue = CalcManThreatValue(pOpponent, pSoldier->sGridNo, FALSE, pSoldier); // if the opponent is no threat at all for some reason if (Threat[uiThreatCnt].iValue == -999) { continue; // check next opponent } Threat[uiThreatCnt].pOpponent = pOpponent; Threat[uiThreatCnt].sGridNo = sThreatLoc; Threat[uiThreatCnt].iCertainty = iThreatCertainty; Threat[uiThreatCnt].iOrigRange = iThreatRange; // calculate how many APs he will have at the start of the next turn Threat[uiThreatCnt].iAPs = pOpponent->CalcActionPoints(); // sevenfm: more information Threat[uiThreatCnt].bLevel = bThreatLevel; Threat[uiThreatCnt].bKnowledge = bKnowledge; Threat[uiThreatCnt].bPersonalKnowledge = bPersonalKnowledge; Threat[uiThreatCnt].bPublicKnowledge = bPublicKnowledge; if (iThreatRange < iClosestThreatRange) { iClosestThreatRange = iThreatRange; } uiThreatCnt++; } return uiThreatCnt; } UINT16 CountPublicKnownEnemies(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT16 sDistance) { CHECKF(pSoldier); UINT32 uiLoop; SOLDIERTYPE *pOpponent; INT32 sThreatLoc; INT8 iThreatLevel; UINT16 ubNum = 0; // loop through all the enemies for (uiLoop = 0; uiLoop < guiNumMercSlots; ++uiLoop) { pOpponent = MercSlots[uiLoop]; // if this merc is inactive, at base, on assignment, dead, unconscious if (!pOpponent || pOpponent->stats.bLife < OKLIFE) { continue; } if (!ValidOpponent(pSoldier, pOpponent)) { continue; } // check public knowledge if (PublicKnowledge(pSoldier->bTeam, pOpponent->ubID) == NOT_HEARD_OR_SEEN) { continue; } sThreatLoc = KnownPublicLocation(pSoldier->bTeam, pOpponent->ubID); iThreatLevel = KnownPublicLevel(pSoldier->bTeam, pOpponent->ubID); if (TileIsOutOfBounds(sThreatLoc)) { continue; } // check distance if (PythSpacesAway(sThreatLoc, sGridNo) > sDistance) { continue; } ubNum++; } return ubNum; } UINT16 CountPublicKnownEnemies(SOLDIERTYPE *pSoldier) { CHECKF(pSoldier); UINT32 uiLoop; SOLDIERTYPE *pOpponent; INT32 sThreatLoc; INT8 iThreatLevel; UINT16 ubNum = 0; // loop through all the enemies for (uiLoop = 0; uiLoop < guiNumMercSlots; ++uiLoop) { pOpponent = MercSlots[uiLoop]; // if this merc is inactive, at base, on assignment, dead, unconscious if (!pOpponent || pOpponent->stats.bLife < OKLIFE) { continue; } if (!ValidOpponent(pSoldier, pOpponent)) { continue; } // check public knowledge if (PublicKnowledge(pSoldier->bTeam, pOpponent->ubID) == NOT_HEARD_OR_SEEN) { continue; } sThreatLoc = KnownPublicLocation(pSoldier->bTeam, pOpponent->ubID); iThreatLevel = KnownPublicLevel(pSoldier->bTeam, pOpponent->ubID); if (TileIsOutOfBounds(sThreatLoc)) { continue; } ubNum++; } return ubNum; } // sevenfm: check if suppression is possible (count friends in the fire direction) BOOLEAN CheckSuppressionDirection(SOLDIERTYPE *pSoldier, INT32 sTargetGridNo, INT8 bTargetLevel) { SOLDIERTYPE * pFriend; UINT8 ubShootingDir; UINT32 uiLoop; CHECKF(pSoldier); CHECKF(!TileIsOutOfBounds(sTargetGridNo)); ubShootingDir = AIDirection(pSoldier->sGridNo, sTargetGridNo); for (uiLoop = 0; uiLoop < guiNumMercSlots; ++uiLoop) { pFriend = MercSlots[uiLoop]; if (pFriend && pFriend != pSoldier && pFriend->bActive && pFriend->bVisible == TRUE && pFriend->stats.bLife >= OKLIFE && (pFriend->bSide == pSoldier->bSide || CONSIDERED_NEUTRAL(pSoldier, pFriend)) && (pFriend->pathing.bLevel == pSoldier->pathing.bLevel || pFriend->pathing.bLevel == bTargetLevel) && ubShootingDir == AIDirection(pSoldier->sGridNo, pFriend->sGridNo) && PythSpacesAway(pSoldier->sGridNo, pFriend->sGridNo) > 1 && PythSpacesAway(pSoldier->sGridNo, pFriend->sGridNo) < 2 * TACTICAL_RANGE && (gAnimControl[pFriend->usAnimState].ubHeight == ANIM_STAND || gGameExternalOptions.fAllowTargetHeadAndLegIfProne) && //!pFriend->IsCowering() && AISoldierToSoldierChanceToGetThrough(pSoldier, pFriend) > 25) //LocationToLocationLineOfSightTest(pSoldier->sGridNo, pSoldier->pathing.bLevel, pFriend->sGridNo, pFriend->pathing.bLevel, TRUE, NO_DISTANCE_LIMIT)) { return FALSE; } } return TRUE; }