diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index f4bd20d3d8..d4a66f2028 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -93,7 +93,7 @@ bool32 AI_CanBattlerEscape(u32 battler); bool32 IsBattlerTrapped(u32 battlerAtk, u32 battlerDef); s32 AI_WhoStrikesFirst(u32 battlerAI, u32 battler2, u32 moveConsidered); bool32 CanTargetFaintAi(u32 battlerDef, u32 battlerAtk); -u32 NoOfHitsForTargetToFaintAI(u32 battlerDef, u32 battlerAtk); +u32 NoOfHitsForTargetToFaintBattler(u32 battlerDef, u32 battlerAtk); u32 GetBestDmgMoveFromBattler(u32 battlerAtk, u32 battlerDef, enum DamageCalcContext calcContext); u32 GetBestDmgFromBattler(u32 battler, u32 battlerTarget, enum DamageCalcContext calcContext); bool32 CanTargetMoveFaintAi(u32 move, u32 battlerDef, u32 battlerAtk, u32 nHits); @@ -115,7 +115,7 @@ bool32 ShouldTryOHKO(u32 battlerAtk, u32 battlerDef, u32 atkAbility, u32 defAbil bool32 ShouldUseRecoilMove(u32 battlerAtk, u32 battlerDef, u32 recoilDmg, u32 moveIndex); u32 GetBattlerSideSpeedAverage(u32 battler); bool32 ShouldAbsorb(u32 battlerAtk, u32 battlerDef, u32 move, s32 damage); -bool32 ShouldRecover(u32 battlerAtk, u32 battlerDef, u32 move, u32 healPercent, enum DamageCalcContext calcContext); +bool32 ShouldRecover(u32 battlerAtk, u32 battlerDef, u32 move, u32 healPercent); bool32 ShouldSetScreen(u32 battlerAtk, u32 battlerDef, enum BattleMoveEffects moveEffect); enum AIPivot ShouldPivot(u32 battlerAtk, u32 battlerDef, u32 defAbility, u32 move, u32 moveIndex); bool32 IsRecycleEncouragedItem(u32 item); diff --git a/include/config/ai.h b/include/config/ai.h index 44d2f3236d..ec359c646d 100644 --- a/include/config/ai.h +++ b/include/config/ai.h @@ -56,6 +56,8 @@ // AI move scoring #define STATUS_MOVE_FOCUS_PUNCH_CHANCE 50 // Chance the AI will use a status move if the player's best move is Focus Punch #define BOOST_INTO_HAZE_CHANCE 0 // Chance the AI will use a stat boosting move if the player has used Haze +#define SHOULD_RECOVER_CHANCE 50 // Chance the AI will give recovery moves score increase if less than ENABLE_RECOVERY_THRESHOLD and in no immediate danger +#define ENABLE_RECOVERY_THRESHOLD 60 // HP percentage beneath which SHOULD_RECOVER_CHANCE is active // AI damage calc considerations #define RISKY_AI_CRIT_STAGE_THRESHOLD 2 // Stat stages at which Risky will assume it gets a crit diff --git a/include/random.h b/include/random.h index 130fb32e69..65e48e1b86 100644 --- a/include/random.h +++ b/include/random.h @@ -200,6 +200,7 @@ enum RandomTag RNG_AI_PREDICT_MOVE, RNG_AI_STATUS_FOCUS_PUNCH, RNG_AI_BOOST_INTO_HAZE, + RNG_AI_SHOULD_RECOVER, RNG_HEALER, RNG_DEXNAV_ENCOUNTER_LEVEL, RNG_AI_ASSUME_STATUS_SLEEP, diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 95c94eb455..5586934eea 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -3897,11 +3897,22 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move) ADJUST_SCORE(DECENT_EFFECT); break; case EFFECT_DREAM_EATER: - case EFFECT_STRENGTH_SAP: case EFFECT_AQUA_RING: if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_BIG_ROOT) ADJUST_SCORE(DECENT_EFFECT); break; + case EFFECT_STRENGTH_SAP: + u32 atkStat = gBattleMons[battlerDef].attack; + u32 atkStage = gBattleMons[battlerDef].statStages[STAT_ATK]; + atkStat *= gStatStageRatios[atkStage][0]; + atkStat /= gStatStageRatios[atkStage][1]; + u32 healPercent = atkStat * 100 / gBattleMons[battlerAtk].maxHP; + if (ShouldRecover(battlerAtk, battlerDef, move, healPercent)) + { + ADJUST_SCORE(GOOD_EFFECT); + if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_BIG_ROOT) + ADJUST_SCORE(WEAK_EFFECT); + } case EFFECT_EXPLOSION: case EFFECT_MISTY_EXPLOSION: case EFFECT_MEMENTO: @@ -4054,7 +4065,7 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move) break; } - if (ShouldRecover(battlerAtk, battlerDef, move, healPercent, AI_DEFENDING)) + if (ShouldRecover(battlerAtk, battlerDef, move, healPercent)) ADJUST_SCORE(DECENT_EFFECT); } break; @@ -4064,7 +4075,7 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move) case EFFECT_MORNING_SUN: case EFFECT_SYNTHESIS: case EFFECT_MOONLIGHT: - if (ShouldRecover(battlerAtk, battlerDef, move, 50, AI_DEFENDING)) + if (ShouldRecover(battlerAtk, battlerDef, move, 50)) ADJUST_SCORE(GOOD_EFFECT); break; case EFFECT_LIGHT_SCREEN: @@ -4082,7 +4093,7 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move) { break; } - else if (ShouldRecover(battlerAtk, battlerDef, move, 100, AI_DEFENDING)) + else if (ShouldRecover(battlerAtk, battlerDef, move, 100)) { if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_CURE_SLP || aiData->holdEffects[battlerAtk] == HOLD_EFFECT_CURE_STATUS @@ -5044,9 +5055,9 @@ case EFFECT_GUARD_SPLIT: ADJUST_SCORE(GOOD_EFFECT); break; case EFFECT_SHORE_UP: - if ((AI_GetWeather() & B_WEATHER_SANDSTORM) && ShouldRecover(battlerAtk, battlerDef, move, 67, AI_DEFENDING)) + if ((AI_GetWeather() & B_WEATHER_SANDSTORM) && ShouldRecover(battlerAtk, battlerDef, move, 67)) ADJUST_SCORE(DECENT_EFFECT); - else if (ShouldRecover(battlerAtk, battlerDef, move, 50, AI_DEFENDING)) + else if (ShouldRecover(battlerAtk, battlerDef, move, 50)) ADJUST_SCORE(DECENT_EFFECT); break; case EFFECT_ENDEAVOR: @@ -5072,8 +5083,8 @@ case EFFECT_GUARD_SPLIT: //case EFFECT_SKY_DROP //break; case EFFECT_JUNGLE_HEALING: - if (ShouldRecover(battlerAtk, battlerDef, move, 25, AI_DEFENDING) - || ShouldRecover(BATTLE_PARTNER(battlerAtk), battlerDef, move, 25, AI_DEFENDING) + if (ShouldRecover(battlerAtk, battlerDef, move, 25) + || ShouldRecover(BATTLE_PARTNER(battlerAtk), battlerDef, move, 25) || gBattleMons[battlerAtk].status1 & STATUS1_ANY || gBattleMons[BATTLE_PARTNER(battlerAtk)].status1 & STATUS1_ANY) ADJUST_SCORE(GOOD_EFFECT); diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index b9e90ee4fd..ff81162d52 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -1349,7 +1349,7 @@ bool32 CanTargetFaintAi(u32 battlerDef, u32 battlerAtk) return FALSE; } -u32 NoOfHitsForTargetToFaintAI(u32 battlerDef, u32 battlerAtk) +u32 NoOfHitsForTargetToFaintBattler(u32 battlerDef, u32 battlerAtk) { u32 i; u32 currNumberOfHits; @@ -1367,6 +1367,32 @@ u32 NoOfHitsForTargetToFaintAI(u32 battlerDef, u32 battlerAtk) return leastNumberOfHits; } +u32 NoOfHitsForTargetToFaintBattlerWithMod(u32 battlerDef, u32 battlerAtk, s32 hpMod) +{ + u32 i; + u32 currNumberOfHits; + u32 leastNumberOfHits = UNKNOWN_NO_OF_HITS; + u32 hpCheck = gBattleMons[battlerAtk].hp + hpMod; + u32 damageDealt = 0; + + if (hpCheck > gBattleMons[battlerAtk].maxHP) + hpCheck = gBattleMons[battlerAtk].maxHP; + + for (i = 0; i < MAX_MON_MOVES; i++) + { + damageDealt = AI_GetDamage(battlerDef, battlerAtk, i, AI_DEFENDING, gAiLogicData); + if (damageDealt == 0) + continue; + currNumberOfHits = hpCheck / (damageDealt + 1) + 1; + if (currNumberOfHits != 0) + { + if (currNumberOfHits < leastNumberOfHits) + leastNumberOfHits = currNumberOfHits; + } + } + return leastNumberOfHits; +} + u32 GetBestDmgMoveFromBattler(u32 battlerAtk, u32 battlerDef, enum DamageCalcContext calcContext) { struct AiLogicData *aiData = gAiLogicData; @@ -3710,21 +3736,30 @@ bool32 ShouldAbsorb(u32 battlerAtk, u32 battlerDef, u32 move, s32 damage) return FALSE; } -bool32 ShouldRecover(u32 battlerAtk, u32 battlerDef, u32 move, u32 healPercent, enum DamageCalcContext calcContext) +bool32 ShouldRecover(u32 battlerAtk, u32 battlerDef, u32 move, u32 healPercent) { - if (move == 0xFFFF || AI_IsFaster(battlerAtk, battlerDef, move)) + u32 maxHP = gBattleMons[battlerAtk].maxHP; + u32 healAmount = (healPercent * maxHP) / 100; + if (healAmount > maxHP) + healAmount = maxHP; + if (gStatuses3[battlerAtk] & STATUS3_HEAL_BLOCK) + healAmount = 0; + if (AI_IsFaster(battlerAtk, battlerDef, move)) { - // using item or user going first - s32 damage = AI_GetDamage(battlerAtk, battlerDef, gAiThinkingStruct->movesetIndex, calcContext, gAiLogicData); - s32 healAmount = (healPercent * damage) / 100; - if (gStatuses3[battlerAtk] & STATUS3_HEAL_BLOCK) - healAmount = 0; - if (CanTargetFaintAi(battlerDef, battlerAtk) && !CanTargetFaintAiWithMod(battlerDef, battlerAtk, healAmount, 0)) return TRUE; // target can faint attacker unless they heal - else if (!CanTargetFaintAi(battlerDef, battlerAtk) && gAiLogicData->hpPercents[battlerAtk] < 60 && (Random() % 3)) - return TRUE; // target can't faint attacker at all, attacker health is about half, 2/3rds rate of encouraging healing + else if (!CanTargetFaintAi(battlerDef, battlerAtk) && gAiLogicData->hpPercents[battlerAtk] < ENABLE_RECOVERY_THRESHOLD && RandomPercentage(RNG_AI_SHOULD_RECOVER, SHOULD_RECOVER_CHANCE)) + return TRUE; // target can't faint attacker at all, generally safe + } + else + { + if (!CanTargetFaintAi(battlerDef, battlerAtk) + && GetBestDmgFromBattler(battlerDef, battlerAtk, AI_DEFENDING) < healAmount + && NoOfHitsForTargetToFaintBattler(battlerDef, battlerAtk) < NoOfHitsForTargetToFaintBattlerWithMod(battlerDef, battlerAtk, healAmount)) + return TRUE; // target can't faint attacker and is dealing less damage than we're healing + else if (!CanTargetFaintAi(battlerDef, battlerAtk) && gAiLogicData->hpPercents[battlerAtk] < ENABLE_RECOVERY_THRESHOLD && RandomPercentage(RNG_AI_SHOULD_RECOVER, SHOULD_RECOVER_CHANCE)) + return TRUE; // target can't faint attacker at all, generally safe } return FALSE; } @@ -3972,7 +4007,7 @@ bool32 ShouldUseWishAromatherapy(u32 battlerAtk, u32 battlerDef, u32 move) switch (GetMoveEffect(move)) { case EFFECT_WISH: - return ShouldRecover(battlerAtk, battlerDef, move, 50, AI_DEFENDING); // Switch recovery isn't good idea in doubles + return ShouldRecover(battlerAtk, battlerDef, move, 50); // Switch recovery isn't good idea in doubles case EFFECT_HEAL_BELL: if (hasStatus) return TRUE; @@ -4272,7 +4307,7 @@ bool32 HasMoveThatChangesKOThreshold(u32 battlerId, u32 noOfHitsToFaint, u32 aiI static enum AIScore IncreaseStatUpScoreInternal(u32 battlerAtk, u32 battlerDef, enum StatChange statId, bool32 considerContrary) { enum AIScore tempScore = NO_INCREASE; - u32 noOfHitsToFaint = NoOfHitsForTargetToFaintAI(battlerDef, battlerAtk); + u32 noOfHitsToFaint = NoOfHitsForTargetToFaintBattler(battlerDef, battlerAtk); u32 aiIsFaster = AI_IsFaster(battlerAtk, battlerDef, TRUE); u32 shouldSetUp = ((noOfHitsToFaint >= 2 && aiIsFaster) || (noOfHitsToFaint >= 3 && !aiIsFaster) || noOfHitsToFaint == UNKNOWN_NO_OF_HITS); u32 i; diff --git a/test/battle/ai/ai.c b/test/battle/ai/ai.c index 5202afe606..bd827fe5a8 100644 --- a/test/battle/ai/ai.c +++ b/test/battle/ai/ai.c @@ -945,3 +945,42 @@ AI_SINGLE_BATTLE_TEST("AI won't setup if otherwise good scenario is changed by t TURN { MOVE(player, MOVE_SURF); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } } } + +AI_SINGLE_BATTLE_TEST("AI will use Recovery move if it outheals your damage and outspeeds") +{ + PASSES_RANDOMLY(100, 100, RNG_AI_SHOULD_RECOVER); + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT); + PLAYER(SPECIES_LINOONE) { Speed(2); Moves(MOVE_HEADBUTT); } + OPPONENT(SPECIES_GASTRODON) { Speed(5); Moves(MOVE_SCALD, MOVE_RECOVER); HP(1); } + } WHEN { + TURN { MOVE(player, MOVE_HEADBUTT); EXPECT_MOVE(opponent, MOVE_RECOVER); } + } +} + +AI_SINGLE_BATTLE_TEST("AI will use recovery move if it outheals your damage and is outsped") +{ + u32 aiMove = MOVE_NONE; + PASSES_RANDOMLY(100, 100, RNG_AI_SHOULD_RECOVER); + PARAMETRIZE{ aiMove = MOVE_RECOVER; } + PARAMETRIZE{ aiMove = MOVE_STRENGTH_SAP; } + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT); + PLAYER(SPECIES_LINOONE) { Speed(5); Moves(MOVE_TACKLE); } + OPPONENT(SPECIES_GASTRODON) { Speed(2); Moves(MOVE_SCALD, aiMove); HP(200); MaxHP(400); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); EXPECT_MOVE(opponent, aiMove); } + } +} + +AI_SINGLE_BATTLE_TEST("AI will use recovery move if is in no immediate danger beneath an HP threshold") +{ + PASSES_RANDOMLY(SHOULD_RECOVER_CHANCE, 100, RNG_AI_SHOULD_RECOVER); + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT); + PLAYER(SPECIES_LINOONE) { Speed(2); Moves(MOVE_TACKLE); } + OPPONENT(SPECIES_GASTRODON) { Speed(5); Moves(MOVE_SCALD, MOVE_RECOVER); HP(200); MaxHP(400); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); EXPECT_MOVE(opponent, MOVE_RECOVER); } + } +}