diff --git a/Tactical/Overhead.cpp b/Tactical/Overhead.cpp index 75ad3c4e..4021a070 100644 --- a/Tactical/Overhead.cpp +++ b/Tactical/Overhead.cpp @@ -3012,6 +3012,96 @@ BOOLEAN HandleAtNewGridNo( SOLDIERTYPE *pSoldier, BOOLEAN *pfKeepMoving ) } } + // sevenfm: check all nearby enemy boxers for opportunity attack + if (IS_MERC_BODY_TYPE(pSoldier) && + (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && + gTacticalStatus.bBoxingState == BOXING && + pSoldier->aiData.bAlertStatus >= STATUS_RED) + { + UINT32 uiLoop; + UINT8 ubChance; + SOLDIERTYPE *pOpponent; + UINT8 ubDirection; + + //loop through all the enemies + for (uiLoop = 0; uiLoop < guiNumMercSlots; ++uiLoop) + { + pOpponent = MercSlots[uiLoop]; + + if (!pOpponent || + SpacesAway(pSoldier->sGridNo, pOpponent->sGridNo) > 1 || + pOpponent->stats.bLife < OKLIFE || + !ValidOpponent(pSoldier, pOpponent) || + pOpponent->bCollapsed || + pOpponent->bBreathCollapsed || + !IS_MERC_BODY_TYPE(pOpponent) || + !(pOpponent->flags.uiStatusFlags & SOLDIER_BOXER) || + gAnimControl[pOpponent->usAnimState].ubEndHeight < ANIM_STAND || + pOpponent->pathing.bLevel != pSoldier->pathing.bLevel || + !SoldierToSoldierLineOfSightTest(pOpponent, pSoldier, TRUE, CALC_FROM_WANTED_DIR) || + pOpponent->bActionPoints <= 0 || + pOpponent->inv[HANDPOS].exists() && pOpponent->inv[SECONDHANDPOS].exists() && !(Item[pOpponent->inv[HANDPOS].usItem].usItemClass & (IC_BLADE | IC_THROWING_KNIFE | IC_PUNCH))) + { + continue; // next merc + } + + ubChance = EffectiveAgility(pSoldier, FALSE) * (100 + pSoldier->bBreath) / 200; + ubDirection = AIDirection(pOpponent->sGridNo, pSoldier->sGridNo); + if (pOpponent->ubDirection == ubDirection) + ubChance = ubChance * 3 / 4; + else if (pOpponent->ubDirection == gOneCDirection[ubDirection] || pOpponent->ubDirection == gOneCCDirection[ubDirection]) + ubChance = ubChance / 2; + else if (pOpponent->ubDirection == gTwoCDirection[ubDirection] || pOpponent->ubDirection == gTwoCCDirection[ubDirection]) + ubChance = ubChance / 4; + else + ubChance = 0; + + if (!Chance(ubChance)) + { + continue; + } + + if (!(Item[pOpponent->inv[HANDPOS].usItem].usItemClass & (IC_BLADE | IC_THROWING_KNIFE | IC_PUNCH)) && + !pOpponent->inv[SECONDHANDPOS].exists()) + { + UINT16 usOldHandItem = pOpponent->inv[HANDPOS].usItem; + SwapHandItems(pOpponent); + pOpponent->ReLoadSoldierAnimationDueToHandItemChange(usOldHandItem, pOpponent->inv[HANDPOS].usItem); + HandleSight(pOpponent, SIGHT_LOOK); + + if (pOpponent->bTeam == gbPlayerNum) + { + fCharacterInfoPanelDirty = TRUE; + fInterfacePanelDirty = DIRTYLEVEL2; + } + } + + // stop soldier + (*pfKeepMoving) = FALSE; + pSoldier->EVENT_StopMerc(pSoldier->sGridNo, pSoldier->ubDirection); + SetNewSituation(pSoldier); + ActionDone(pSoldier); + + // prepare attack + pOpponent->aiData.bAimTime = 0; + if (pOpponent->bActionPoints >= MinAPsToAttack(pOpponent, pSoldier->sGridNo, TRUE, 1, 0) && Chance(pOpponent->stats.bLife)) + pOpponent->aiData.bAimTime = 1; + + pOpponent->bAimShotLocation = AIM_SHOT_RANDOM; + if (gAnimControl[pSoldier->usAnimState].ubEndHeight > ANIM_PRONE) + { + if (Chance((6 + EffectiveDexterity(pOpponent, FALSE) / 10 + 5 * NUM_SKILL_TRAITS(pOpponent, MARTIAL_ARTS_NT)) * 100 / (100 + pSoldier->bBreath))) + pOpponent->bAimShotLocation = AIM_SHOT_HEAD; + else if (Chance(pSoldier->bBreath * EffectiveWisdom(pOpponent) / (100 + EffectiveDexterity(pOpponent, FALSE)))) + pOpponent->bAimShotLocation = AIM_SHOT_LEGS; + else + pOpponent->bAimShotLocation = AIM_SHOT_TORSO; + } + + HandleItem(pOpponent, pSoldier->sGridNo, pOpponent->pathing.bLevel, pOpponent->inv[HANDPOS].usItem, FALSE); + } + } + HandleSystemNewAISituation( pSoldier, FALSE ); // Flugente: tracker... diff --git a/Tactical/Soldier Control.cpp b/Tactical/Soldier Control.cpp index 9cfc5ac1..211703da 100644 --- a/Tactical/Soldier Control.cpp +++ b/Tactical/Soldier Control.cpp @@ -7891,6 +7891,10 @@ void SOLDIERTYPE::EVENT_BeginMercTurn( BOOLEAN fFromRealTime, INT32 iRealTimeCou this->usSoldierFlagMask2 &= ~SOLDIER_TAKEN_LARGE_HIT; } + // sevenfm: reset AI flags + this->usSoldierFlagMask2 &= ~SOLDIER_BACK_ATTACK; + this->usSoldierFlagMask2 &= ~SOLDIER_SNEAK_ATTACK; + // Flugente: reset extra stats. Currently they only depend on drug effects, and those are reset every turn this->ResetExtraStats( ); @@ -12780,13 +12784,32 @@ void SOLDIERTYPE::EVENT_SoldierBeginBladeAttack( INT32 sGridNo, UINT8 ubDirectio { GetSoldier( &pTSoldier, usSoldierIndex ); + // sevenfm: set flag indicating back attack and sneak attack + if (pTSoldier) + { + pTSoldier->usSoldierFlagMask2 &= ~SOLDIER_BACK_ATTACK; + pTSoldier->usSoldierFlagMask2 &= ~SOLDIER_SNEAK_ATTACK; + + UINT8 ubAttackDirection = AIDirection(this->sGridNo, pTSoldier->sGridNo); + if (ubAttackDirection == pTSoldier->ubDirection || + ubAttackDirection == gOneCDirection[pTSoldier->ubDirection] || + ubAttackDirection == gOneCCDirection[pTSoldier->ubDirection]) + { + pTSoldier->usSoldierFlagMask2 |= SOLDIER_BACK_ATTACK; + } + + if (pTSoldier->aiData.bOppList[this->ubID] != SEEN_CURRENTLY && + pTSoldier->aiData.bOppList[this->ubID] != SEEN_THIS_TURN && + pTSoldier->aiData.bOppList[this->ubID] != HEARD_THIS_TURN) + { + pTSoldier->usSoldierFlagMask2 |= SOLDIER_SNEAK_ATTACK; + } + } + // Flugente: if we attack with a bayonet, we don't need to change stance if even if we are standing and the target is prone... // so we simulate here that the target is still standing UINT8 targetheight = gAnimControl[pTSoldier->usAnimState].ubEndHeight; -#if 0//dnl ch73 031013 several reasons why disabling this; 1. no animation for bayonet, 2. if target is prone it look ridicules to swing through air instead stub target, 3. incorrect APs calculation - if ( this->bWeaponMode == WM_ATTACHED_BAYONET ) - targetheight = ANIM_STAND; -#endif + // Look at stance of target switch ( targetheight ) { @@ -12927,8 +12950,10 @@ void SOLDIERTYPE::EVENT_SoldierBeginPunchAttack( INT32 sGridNo, UINT8 ubDirectio //} - // get target..... - usSoldierIndex = WhoIsThere2( this->sTargetGridNo, this->pathing.bLevel ); + // sevenfm: use supplied gridno as sTargetGridNo may not be set yet + //usSoldierIndex = WhoIsThere2( this->sTargetGridNo, this->pathing.bLevel ); + usSoldierIndex = WhoIsThere2(sGridNo, this->pathing.bLevel); + if ( usSoldierIndex != NOBODY ) { GetSoldier( &pTSoldier, usSoldierIndex ); @@ -12940,6 +12965,28 @@ void SOLDIERTYPE::EVENT_SoldierBeginPunchAttack( INT32 sGridNo, UINT8 ubDirectio return; } + // sevenfm: set flag indicating back attack and sneak attack + if (pTSoldier) + { + pTSoldier->usSoldierFlagMask2 &= ~SOLDIER_BACK_ATTACK; + pTSoldier->usSoldierFlagMask2 &= ~SOLDIER_SNEAK_ATTACK; + + UINT8 ubAttackDirection = AIDirection(this->sGridNo, pTSoldier->sGridNo); + if (ubAttackDirection == pTSoldier->ubDirection || + ubAttackDirection == gOneCDirection[pTSoldier->ubDirection] || + ubAttackDirection == gOneCCDirection[pTSoldier->ubDirection]) + { + pTSoldier->usSoldierFlagMask2 |= SOLDIER_BACK_ATTACK; + } + + if (pTSoldier->aiData.bOppList[this->ubID] != SEEN_CURRENTLY && + pTSoldier->aiData.bOppList[this->ubID] != SEEN_THIS_TURN && + pTSoldier->aiData.bOppList[this->ubID] != HEARD_THIS_TURN) + { + pTSoldier->usSoldierFlagMask2 |= SOLDIER_SNEAK_ATTACK; + } + } + if ( fChangeDirection ) { // CHANGE DIRECTION AND GOTO ANIMATION NOW diff --git a/Tactical/Soldier Control.h b/Tactical/Soldier Control.h index f656d3ea..2aa78769 100644 --- a/Tactical/Soldier Control.h +++ b/Tactical/Soldier Control.h @@ -429,6 +429,8 @@ enum #define SOLDIER_DRAG_SOUND 0x00080000 // played sound when started dragging #define SOLDIER_SPENT_AP 0x00100000 // soldier has spent some AP this turn (including realtime) #define SOLDIER_TURNCOAT 0x00200000 // this enemy soldier will switch to the militia team if ordered to +#define SOLDIER_BACK_ATTACK 0x00400000 // soldier was attacked from the back +#define SOLDIER_SNEAK_ATTACK 0x00800000 // soldier was attacked by unseen enemy #define SOLDIER_INTERROGATE_ALL 0x000001F8 // all interrogation flags // ---------------------------------------------------------------- diff --git a/Tactical/Weapons.cpp b/Tactical/Weapons.cpp index 7e91a1b2..9e7b00de 100644 --- a/Tactical/Weapons.cpp +++ b/Tactical/Weapons.cpp @@ -4091,7 +4091,9 @@ BOOLEAN UseHandToHand( SOLDIERTYPE *pSoldier, INT32 sTargetGridNo, BOOLEAN fStea { iHitChance = 0; } - else if ( pTargetSoldier->aiData.bOppList[ pSoldier->ubID ] == NOT_HEARD_OR_SEEN ) + // sevenfm: use sneak attack code + //else if ( pTargetSoldier->aiData.bOppList[ pSoldier->ubID ] == NOT_HEARD_OR_SEEN ) + else if (pTargetSoldier->usSoldierFlagMask2 & SOLDIER_SNEAK_ATTACK) { // give bonus for surprise, but not so much as struggle would still occur iHitChance = CalcChanceToSteal( pSoldier, pTargetSoldier, pSoldier->aiData.bAimTime ) + 20; @@ -4108,13 +4110,26 @@ BOOLEAN UseHandToHand( SOLDIERTYPE *pSoldier, INT32 sTargetGridNo, BOOLEAN fStea } else { - if ( pTargetSoldier->aiData.bOppList[ pSoldier->ubID ] == NOT_HEARD_OR_SEEN || pTargetSoldier->stats.bLife < OKLIFE || pTargetSoldier->bCollapsed ) + // sevenfm: use sneak attack code + //if ( pTargetSoldier->aiData.bOppList[ pSoldier->ubID ] == NOT_HEARD_OR_SEEN || pTargetSoldier->stats.bLife < OKLIFE || pTargetSoldier->bCollapsed ) + if (pTargetSoldier->usSoldierFlagMask2 & SOLDIER_SNEAK_ATTACK || pTargetSoldier->stats.bLife < OKLIFE || pTargetSoldier->bCollapsed) { iHitChance = 100; } else { iHitChance = CalcChanceToPunch( pSoldier, pTargetSoldier, pSoldier->aiData.bAimTime ); + + // sevenfm: bonus for boxers for attack from the back + if (iHitChance < 100 && + (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && + !pSoldier->bBlindedCounter && + gAnimControl[pTargetSoldier->usAnimState].ubEndHeight > ANIM_PRONE && + (pTargetSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && + pTargetSoldier->usSoldierFlagMask2 & SOLDIER_BACK_ATTACK) + { + iHitChance += (100 - iHitChance) / 2; + } } } @@ -4714,13 +4729,59 @@ BOOLEAN UseHandToHand( SOLDIERTYPE *pSoldier, INT32 sTargetGridNo, BOOLEAN fStea PossiblyStartEnemyTaunt( pSoldier, TAUNT_MISS_HTH, pTargetSoldier->ubID ); if( pTargetSoldier->aiData.bOppList[ pSoldier->ubID ] == SEEN_CURRENTLY ) PossiblyStartEnemyTaunt( pTargetSoldier, TAUNT_GOT_MISSED_HTH, pSoldier->ubID ); + + //INT16 sMinAPCost = MinAPsToAttack(pTargetSoldier, pSoldier->sGridNo, TRUE, 0, 0); + UINT8 ubCounterattackChance = EffectiveDexterity(pTargetSoldier, FALSE) * (100 + pTargetSoldier->bBreath) / 200; + + // halve chance for counterattack if boxer was hit recently + if (pTargetSoldier->usSoldierFlagMask2 & SOLDIER_TAKEN_LARGE_HIT) + ubCounterattackChance /= 2; + + // sevenfm: possibly counterattack when boxing + if (pTargetSoldier->bActionPoints > 0 && + gGameOptions.fNewTraitSystem && + gTacticalStatus.bBoxingState == BOXING && + (pTargetSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && + Chance(ubCounterattackChance) && + IS_MERC_BODY_TYPE(pSoldier) && + IS_MERC_BODY_TYPE(pTargetSoldier) && + gAnimControl[pTargetSoldier->usAnimState].ubEndHeight == ANIM_STAND && + !(pTargetSoldier->usSoldierFlagMask2 & SOLDIER_BACK_ATTACK) && + (!pTargetSoldier->inv[HANDPOS].exists() || Item[pTargetSoldier->inv[HANDPOS].usItem].usItemClass & (IC_BLADE | IC_THROWING_KNIFE | IC_PUNCH) || !pTargetSoldier->inv[SECONDHANDPOS].exists())) + { + if (!(Item[pTargetSoldier->inv[HANDPOS].usItem].usItemClass & (IC_BLADE | IC_THROWING_KNIFE | IC_PUNCH)) && + !pTargetSoldier->inv[SECONDHANDPOS].exists()) + { + UINT16 usOldHandItem = pTargetSoldier->inv[HANDPOS].usItem; + SwapHandItems(pTargetSoldier); + pTargetSoldier->ReLoadSoldierAnimationDueToHandItemChange(usOldHandItem, pTargetSoldier->inv[HANDPOS].usItem); + HandleSight(pTargetSoldier, SIGHT_LOOK); + + if (pTargetSoldier->bTeam == gbPlayerNum) + { + fCharacterInfoPanelDirty = TRUE; + fInterfacePanelDirty = DIRTYLEVEL2; + } + } + + pTargetSoldier->aiData.bAimTime = 0; + if (pTargetSoldier->bActionPoints >= MinAPsToAttack(pTargetSoldier, pSoldier->sGridNo, TRUE, 1, 0) && Chance(EffectiveStrength(pTargetSoldier, FALSE) / 4)) + pTargetSoldier->aiData.bAimTime = 1; + + pTargetSoldier->bAimShotLocation = AIM_SHOT_RANDOM; + if (gAnimControl[pSoldier->usAnimState].ubEndHeight > ANIM_PRONE) + { + if (Chance((6 + EffectiveDexterity(pTargetSoldier, FALSE) / 10 + 5 * NUM_SKILL_TRAITS(pTargetSoldier, MARTIAL_ARTS_NT)) * 100 / (100 + pSoldier->bBreath))) + pTargetSoldier->bAimShotLocation = AIM_SHOT_HEAD; + else if (Chance(pSoldier->bBreath * EffectiveWisdom(pTargetSoldier) / (100 + EffectiveDexterity(pTargetSoldier, FALSE)))) + pTargetSoldier->bAimShotLocation = AIM_SHOT_LEGS; + else + pTargetSoldier->bAimShotLocation = AIM_SHOT_TORSO; + } + + HandleItem(pTargetSoldier, pSoldier->sGridNo, pTargetSoldier->pathing.bLevel, pTargetSoldier->inv[HANDPOS].usItem, FALSE); + } } - // 0verhaul: And this too - // else - // { - // DebugMsg( TOPIC_JA2, DBG_LEVEL_3, String("@@@@@@@ Freeing up attacker - missed in HTH attack") ); - // FreeUpAttacker( (UINT8) pSoldier->ubID ); - // } } } diff --git a/TacticalAI/AIUtils.cpp b/TacticalAI/AIUtils.cpp index c01416e7..50be8986 100644 --- a/TacticalAI/AIUtils.cpp +++ b/TacticalAI/AIUtils.cpp @@ -1385,7 +1385,7 @@ INT32 ClosestReachableDisturbance(SOLDIERTYPE *pSoldier, BOOLEAN * pfChangeLevel } -INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel) +INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel, UINT8 *pubOpponentID) { INT32 *psLastLoc,sGridNo, sClosestOpponent = NOWHERE; UINT32 uiLoop; @@ -1397,6 +1397,11 @@ INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLev bClosestLevel = -1; + if (pubOpponentID) + { + *pubOpponentID = NOBODY; + } + // NOTE: THIS FUNCTION ALLOWS RETURN OF UNCONSCIOUS AND UNREACHABLE OPPONENTS psLastLoc = &(gsLastKnownOppLoc[pSoldier->ubID][0]); @@ -1500,6 +1505,10 @@ INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLev { *pbLevel = bClosestLevel; } + if (pubOpponentID && pClosestOpponent) + { + *pubOpponentID = pClosestOpponent->ubID; + } return( sClosestOpponent ); } diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 10567903..f11ef71d 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -37,6 +37,7 @@ #include "Text.h" #include "Exit Grids.h" // added by Flugente #include "Game Clock.h" // sevenfm +#include "SkillCheck.h" // sevenfm ////////////////////////////////////////////////////////////////////////////// // SANDRO - In this file, all APBPConstants[AP_CROUCH] and APBPConstants[AP_PRONE] were changed to GetAPsCrouch() and GetAPsProne() @@ -702,6 +703,11 @@ INT8 DecideActionGreen(SOLDIERTYPE *pSoldier) DebugMsg(TOPIC_JA2,DBG_LEVEL_3,String("DecideActionGreen, orders = %d",pSoldier->aiData.bOrders)); + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + BOOLEAN fCivilian = (PTR_CIVILIAN && (pSoldier->ubCivilianGroup == NON_CIV_GROUP || pSoldier->aiData.bNeutral || (pSoldier->ubBodyType >= FATCIV && pSoldier->ubBodyType <= CRIPPLECIV) ) ); BOOLEAN fCivilianOrMilitia = PTR_CIV_OR_MILITIA; @@ -1545,6 +1551,11 @@ INT8 DecideActionYellow(SOLDIERTYPE *pSoldier) STR16 tempstr; #endif + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + if (fCivilian || (gGameExternalOptions.fAllNamedNpcsDecideAction && pSoldier->ubProfile != NO_PROFILE)) { if (pSoldier->flags.uiStatusFlags & SOLDIER_COWERING) @@ -2457,6 +2468,11 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) DebugMsg (TOPIC_JA2,DBG_LEVEL_3,String("DecideActionRed: soldier orders = %d",pSoldier->aiData.bOrders)); + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // if we have absolutely no action points, we can't do a thing under RED! if ( pSoldier->bActionPoints <= 0 ) //Action points can be negative { @@ -2593,7 +2609,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) //if (fCivilian && !(pSoldier->ubBodyType == COW || pSoldier->ubBodyType == CRIPPLECIV || pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE) && gTacticalStatus.bBoxingState == NOT_BOXING) if (fCivilian && !(pSoldier->ubBodyType == COW || pSoldier->ubBodyType == CRIPPLECIV || pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE)) { - if ( FindAIUsableObjClass( pSoldier, IC_WEAPON ) == ITEM_NOT_FOUND ) + if (FindAIUsableObjClass(pSoldier, IC_WEAPON) == NO_SLOT) { // cower in fear!! if ( pSoldier->flags.uiStatusFlags & SOLDIER_COWERING ) @@ -4579,6 +4595,11 @@ INT16 ubMinAPCost; DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"DecideActionBlack"); + // sevenfm: disable stealth mode + pSoldier->bStealthMode = FALSE; + // disable reverse movement mode + pSoldier->bReverse = FALSE; + // sevenfm: stop flanking when we see enemy if( AICheckIsFlanking(pSoldier) ) { @@ -4674,7 +4695,9 @@ INT16 ubMinAPCost; bInGas = FALSE; // calculate our morale - pSoldier->aiData.bAIMorale = CalcMorale(pSoldier); + // sevenfm: for boxer, always use high morale + //pSoldier->aiData.bAIMorale = CalcMorale(pSoldier); + pSoldier->aiData.bAIMorale = MORALE_FEARLESS; // and continue on... } else //???? @@ -4835,7 +4858,6 @@ INT16 ubMinAPCost; // SOLDIER CAN ATTACK IF NOT IN WATER/GAS AND NOT DOING SOMETHING TOO FUNKY //////////////////////////////////////////////////////////////////////////// - // NPCs in water/tear gas without masks are not permitted to shoot/stab/throw if ((pSoldier->bActionPoints < 2) || bInDeepWater || bInGas || pSoldier->aiData.bRTPCombat == RTP_COMBAT_REFRAIN) { @@ -4850,7 +4872,6 @@ INT16 ubMinAPCost; { do { - bCanAttack = CanNPCAttack(pSoldier); if (bCanAttack != TRUE) { @@ -5322,16 +5343,81 @@ INT16 ubMinAPCost; // CHOOSE THE BEST TYPE OF ATTACK OUT OF THOSE FOUND TO BE POSSIBLE ////////////////////////////////////////////////////////////////////////// DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"CHOOSE THE BEST TYPE OF ATTACK OUT OF THOSE FOUND TO BE POSSIBLE"); + BestAttack.iAttackValue = 0; + if (BestShot.ubPossible) { BestAttack.iAttackValue = BestShot.iAttackValue; ubBestAttackAction = AI_ACTION_FIRE_GUN; DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"best action = fire gun"); } - else + + // cautious boxer approach, reserve AP for two attacks (only if not attacking from the back) + if (BestStab.ubPossible && + (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && + SpacesAway(pSoldier->sGridNo, BestStab.sTarget) > 2 && + BestStab.ubOpponent != NOBODY && + MercPtrs[BestStab.ubOpponent] && + AIDirection(pSoldier->sGridNo, MercPtrs[BestStab.ubOpponent]->sGridNo) != MercPtrs[BestStab.ubOpponent]->ubDirection && + AIDirection(pSoldier->sGridNo, MercPtrs[BestStab.ubOpponent]->sGridNo) != gOneCDirection[MercPtrs[BestStab.ubOpponent]->ubDirection] && + AIDirection(pSoldier->sGridNo, MercPtrs[BestStab.ubOpponent]->sGridNo) != gOneCCDirection[MercPtrs[BestStab.ubOpponent]->ubDirection] && + pSoldier->bInitialActionPoints >= 2 * MinAPsToAttack(pSoldier, pSoldier->sLastTarget, FALSE, 0, 0) + APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_WALK] && + pSoldier->bActionPoints < BestStab.ubAPCost + MinAPsToAttack(pSoldier, pSoldier->sLastTarget, FALSE, 0, 0)) { - BestAttack.iAttackValue = 0; + BestStab.ubPossible = FALSE; + fTryPunching = FALSE; + DebugAI(AI_MSG_INFO, pSoldier, String("boxer cannot reserve APs for second attack - disable stab attack")); } + + // try to avoid frontal attack + if (BestStab.ubPossible && + (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && + SpacesAway(pSoldier->sGridNo, BestStab.sTarget) > 1 && + BestStab.ubOpponent != NOBODY && + MercPtrs[BestStab.ubOpponent] && + gAnimControl[MercPtrs[BestStab.ubOpponent]->usAnimState].ubEndHeight == ANIM_STAND && + MercPtrs[BestStab.ubOpponent]->bActionPoints > 0 && + Chance(EffectiveAgility(MercPtrs[BestStab.ubOpponent], FALSE) * (100 + MercPtrs[BestStab.ubOpponent]->bBreath) * EffectiveWisdom(pSoldier) / (100 * 200))) + { + // find closest spot around opponent, avoid front direction + UINT8 ubMovementCost; + INT32 sTempGridNo; + UINT8 ubDirection; + INT32 sPathCost; + INT32 sBestSpot = NOWHERE; + INT32 sBestPathCost = 0; + + for (ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++) + { + sTempGridNo = NewGridNo(BestStab.sTarget, DirectionInc(ubDirection)); + + if (sTempGridNo != BestStab.sTarget) + { + ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][pSoldier->pathing.bLevel]; + + if (ubMovementCost < TRAVELCOST_BLOCKED && + NewOKDestination(pSoldier, sTempGridNo, FALSE, pSoldier->pathing.bLevel) && + AIDirection(BestStab.sTarget, sTempGridNo) != MercPtrs[BestStab.ubOpponent]->ubDirection) + { + sPathCost = PlotPath(pSoldier, sTempGridNo, FALSE, FALSE, FALSE, DetermineMovementMode(pSoldier, AI_ACTION_GET_CLOSER), pSoldier->bStealthMode, pSoldier->bReverse, 0); + if (TileIsOutOfBounds(sBestSpot) || sPathCost < sBestPathCost) + { + sBestSpot = sTempGridNo; + sBestPathCost = sPathCost; + } + } + } + } + + if (!TileIsOutOfBounds(sBestSpot) && + pSoldier->bActionPoints >= sPathCost + GetAPsToLook(pSoldier) + MinAPsToAttack(pSoldier, pSoldier->sLastTarget, FALSE, 0, 0)) + { + pSoldier->aiData.usActionData = sBestSpot; + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: get closer to opponent, avoid front direction")); + return(AI_ACTION_GET_CLOSER); + } + } + if (BestStab.ubPossible && ((BestStab.iAttackValue > BestAttack.iAttackValue) || (ubBestAttackAction == AI_ACTION_NONE))) { BestAttack.iAttackValue = BestStab.iAttackValue; @@ -6100,34 +6186,147 @@ L_NEWAIM: // try to make boxer close if possible if (pSoldier->flags.uiStatusFlags & SOLDIER_BOXER ) { - if ( ubCanMove ) + DebugAI(AI_MSG_TOPIC, pSoldier, String("[Make boxer close if possible]")); + + UINT8 ubOpponentID; + sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL, &ubOpponentID); + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: found closest opponent [%d] at %d", ubOpponentID, sClosestOpponent)); + + if (!TileIsOutOfBounds(sClosestOpponent) && + ubOpponentID != NOBODY && + MercPtrs[ubOpponentID]) { - sClosestOpponent = ClosestSeenOpponent(pSoldier, NULL, NULL); - - if (!TileIsOutOfBounds(sClosestOpponent)) + if (pSoldier->bActionPoints > 0) { - // temporarily make boxer have orders of CLOSEPATROL rather than STATIONARY - // And make him patrol the ring, not his usual place - // so he has a good roaming range - USHORT tgrd = pSoldier->aiData.sPatrolGrid[0]; - pSoldier->aiData.sPatrolGrid[0] = pSoldier->sGridNo; - pSoldier->aiData.bOrders = CLOSEPATROL; - pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards( pSoldier, sClosestOpponent, AI_ACTION_GET_CLOSER ); - pSoldier->aiData.sPatrolGrid[0] = tgrd; - pSoldier->aiData.bOrders = STATIONARY; - - if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + if (SpacesAway(pSoldier->sGridNo, sClosestOpponent) > 1) { - // truncate path to 1 step - pSoldier->aiData.usActionData = pSoldier->sGridNo + DirectionInc( (UINT8) pSoldier->pathing.usPathingData[0] ); - pSoldier->pathing.sFinalDestination = pSoldier->aiData.usActionData; - pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; - return( AI_ACTION_GET_CLOSER ); + INT16 sReserveAP = GetAPsToLook(pSoldier) + 2 * MinAPsToAttack(pSoldier, pSoldier->sLastTarget, FALSE, 0, 0); + BOOLEAN fLimitOneStep = FALSE; + + if (pSoldier->bInitialActionPoints < sReserveAP + APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_WALK]) + { + sReserveAP = GetAPsToLook(pSoldier) + MinAPsToAttack(pSoldier, pSoldier->sLastTarget, FALSE, 0, 0); + } + + if (pSoldier->bInitialActionPoints < sReserveAP + APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_WALK] && + pSoldier->bInitialActionPoints >= GetAPsToLook(pSoldier) + MinAPsToAttack(pSoldier, pSoldier->sLastTarget, FALSE, 0, 0) && + SpacesAway(pSoldier->sGridNo, sClosestOpponent) > 2) + { + sReserveAP = GetAPsToLook(pSoldier) + 1; + fLimitOneStep = TRUE; + } + + // temporarily make boxer have orders of CLOSEPATROL rather than STATIONARY + // And make him patrol the ring, not his usual place + // so he has a good roaming range + INT32 tgrd = pSoldier->aiData.sPatrolGrid[0]; + pSoldier->aiData.sPatrolGrid[0] = pSoldier->sGridNo; + pSoldier->aiData.bOrders = CLOSEPATROL; + //pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards( pSoldier, sClosestOpponent, AI_ACTION_GET_CLOSER ); + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestOpponent, sReserveAP, AI_ACTION_GET_CLOSER, 0); + pSoldier->aiData.sPatrolGrid[0] = tgrd; + pSoldier->aiData.bOrders = STATIONARY; + + // decide to restore breath + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData) && + (pSoldier->bBreath < OKBREATH || + pSoldier->bBreath < pSoldier->bBreathMax && + pSoldier->bBreath < MercPtrs[ubOpponentID]->bBreath && + Chance((100 - pSoldier->bBreath) * (100 - pSoldier->bBreath) / (2 * 100 * 100)))) + { + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: restore breath")); + pSoldier->aiData.usActionData = NOWHERE; + } + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + // truncate path to 1 step + if (fLimitOneStep) + { + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: limit movement to one step")); + pSoldier->aiData.usActionData = pSoldier->sGridNo + DirectionInc((UINT8)pSoldier->pathing.usPathingData[0]); + pSoldier->pathing.sFinalDestination = pSoldier->aiData.usActionData; + } + + //pSoldier->aiData.bNextAction = AI_ACTION_END_TURN; + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: get closer to opponent")); + return(AI_ACTION_GET_CLOSER); + } + } + else if (pSoldier->bBreath < OKBREATH || + pSoldier->bBreath < pSoldier->bBreathMax && + (pSoldier->bBreath < MercPtrs[ubOpponentID]->bBreath || !pSoldier->aiData.bLastAttackHit && (pSoldier->usSoldierFlagMask2 & SOLDIER_TAKEN_LARGE_HIT))) + { + // maybe move away from opponent + UINT8 ubOpponentDir = AIDirection(pSoldier->sGridNo, sClosestOpponent); + INT32 sCheckGridNo = NewGridNo(pSoldier->sGridNo, DirectionInc(gOppositeDirection[ubOpponentDir])); + + // only use reverse movement if we are facing opponent + if (pSoldier->ubDirection == ubOpponentDir || + pSoldier->ubDirection == gOneCDirection[ubOpponentDir] || + pSoldier->ubDirection == gOneCCDirection[ubOpponentDir]) + { + pSoldier->bReverse = TRUE; + } + + if (!NewOKDestination(pSoldier, sCheckGridNo, FALSE, pSoldier->pathing.bLevel) || + PlotPath(pSoldier, sCheckGridNo, FALSE, FALSE, FALSE, DetermineMovementMode(pSoldier, AI_ACTION_TAKE_COVER), pSoldier->bStealthMode, pSoldier->bReverse, 0) > pSoldier->bActionPoints - 1) + { + //if (sPathcost > pSoldier->bActionPoints - (GetAPsToLook(pSoldier) + 1)) + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: bad destination or high path cost, cannot move away")); + sCheckGridNo = NOWHERE; + } + + // maybe try diagonal movement + if (TileIsOutOfBounds(sCheckGridNo) && Chance(50)) + { + sCheckGridNo = NewGridNo(pSoldier->sGridNo, DirectionInc(gOneCDirection[gOppositeDirection[ubOpponentDir]])); + if (!NewOKDestination(pSoldier, sCheckGridNo, FALSE, pSoldier->pathing.bLevel) || + PlotPath(pSoldier, sCheckGridNo, FALSE, FALSE, FALSE, DetermineMovementMode(pSoldier, AI_ACTION_TAKE_COVER), pSoldier->bStealthMode, pSoldier->bReverse, 0) > pSoldier->bActionPoints - 1) + { + //if (sPathcost > pSoldier->bActionPoints - (GetAPsToLook(pSoldier) + 1)) + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: bad destination or high path cost, cannot move away")); + sCheckGridNo = NOWHERE; + } + } + if (TileIsOutOfBounds(sCheckGridNo) && Chance(50)) + { + sCheckGridNo = NewGridNo(pSoldier->sGridNo, DirectionInc(gOneCCDirection[gOppositeDirection[ubOpponentDir]])); + if (!NewOKDestination(pSoldier, sCheckGridNo, FALSE, pSoldier->pathing.bLevel) || + PlotPath(pSoldier, sCheckGridNo, FALSE, FALSE, FALSE, DetermineMovementMode(pSoldier, AI_ACTION_TAKE_COVER), pSoldier->bStealthMode, pSoldier->bReverse, 0) > pSoldier->bActionPoints - 1) + { + //if (sPathcost > pSoldier->bActionPoints - (GetAPsToLook(pSoldier) + 1)) + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: bad destination or high path cost, cannot move away")); + sCheckGridNo = NOWHERE; + } + } + + if (!TileIsOutOfBounds(sCheckGridNo)) + { + pSoldier->aiData.usActionData = sCheckGridNo; + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: get away from opponent")); + return(AI_ACTION_TAKE_COVER); + } + pSoldier->bReverse = FALSE; } } + + UINT8 ubOpponentDir = AIDirection(pSoldier->sGridNo, sClosestOpponent); + + // possibly turn to closest opponent + if (pSoldier->ubDirection != ubOpponentDir && + pSoldier->InternalIsValidStance(ubOpponentDir, gAnimControl[pSoldier->usAnimState].ubEndHeight) && + pSoldier->bActionPoints >= GetAPsToLook(pSoldier)) + { + pSoldier->aiData.usActionData = ubOpponentDir; + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: turn to closest opponent")); + return(AI_ACTION_CHANGE_FACING); + } } + // otherwise do nothing - return( AI_ACTION_NONE ); + DebugAI(AI_MSG_INFO, pSoldier, String("boxer: nothing to do")); + return(AI_ACTION_NONE); } //////////////////////////////////////////////////////////////////////////// diff --git a/TacticalAI/FindLocations.cpp b/TacticalAI/FindLocations.cpp index 4e628283..4fd3f687 100644 --- a/TacticalAI/FindLocations.cpp +++ b/TacticalAI/FindLocations.cpp @@ -403,19 +403,20 @@ INT32 CalcCoverValue(SOLDIERTYPE *pMe, INT32 sMyGridNo, INT32 iMyThreat, INT32 i bMyCTGT = CalcAverageCTGTForPosition( pMe, pHim->ubID, sHisGridNo, pHim->pathing.bLevel, iMyAPsLeft ); gUnderFire.Disable(); ubFriendlyFireChance = gUnderFire.Chance(pMe->bTeam, pMe->bSide, TRUE); - - // sevenfm: penalize position if friendly fire chance is high - if (gGameExternalOptions.fAIBetterCover && ubFriendlyFireChance > MIN_CHANCE_TO_ACCIDENTALLY_HIT_SOMEONE) + + if (gGameExternalOptions.fAIBetterCover) { - bMyCTGT = 1; - } - - // since NPCs are too dumb to shoot "blind", ie. at opponents that they - // themselves can't see (mercs can, using another as a spotter!), if the - // cover is below the "see_thru" threshold, it's equivalent to perfect cover! - if (bMyCTGT < SEE_THRU_COVER_THRESHOLD) + // sevenfm: penalize position if friendly fire chance is high + if (ubFriendlyFireChance > MIN_CHANCE_TO_ACCIDENTALLY_HIT_SOMEONE) + bMyCTGT = 1; + } + else { - bMyCTGT = 0; + // since NPCs are too dumb to shoot "blind", ie. at opponents that they + // themselves can't see (mercs can, using another as a spotter!), if the + // cover is below the "see_thru" threshold, it's equivalent to perfect cover! + if (bMyCTGT < SEE_THRU_COVER_THRESHOLD) + bMyCTGT = 0; } } diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index 9f1cab50..747d03fa 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -175,7 +175,7 @@ void CheckForChangingOrders(SOLDIERTYPE *pSoldier ); INT8 ClosestPanicTrigger( SOLDIERTYPE * pSoldier ); -INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel); +INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLevel, UINT8 *pubOpponentID = NULL); INT32 ClosestPC( SOLDIERTYPE *pSoldier, INT32 * psDistance ); INT32 ClosestUnDisguisedPC( SOLDIERTYPE *pSoldier, INT32 * psDistance ); // Flugente: like ClosestPC(...), but does not account for covert or not visible mercs BOOLEAN CanAutoBandage( BOOLEAN fDoFullCheck );