From 8d58af4d333ff8f18283ded2d7cf8631e4485885 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Mon, 4 Mar 2024 09:54:04 +0100 Subject: [PATCH] Move most damage AI_BadMove checks to AI_CalcDamage (#4238) * Move a couple damage AI_BadMove checks to AI_CalcDamage * re-add effectivness score decrease * reduce score for bad move in ai_checkviability * review changes --- src/battle_ai_main.c | 130 ++++++++++--------------------------------- src/battle_ai_util.c | 79 +++++++++++++++++++++++++- test/battle/ai.c | 25 +++++++++ 3 files changed, 131 insertions(+), 103 deletions(-) diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 99e1f8c3c8..4eb608d7e4 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -798,47 +798,30 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) SetTypeBeforeUsingMove(move, battlerAtk); GET_MOVE_TYPE(move, moveType); + if (gMovesInfo[move].powderMove && !IsAffectedByPowder(battlerDef, aiData->abilities[battlerDef], aiData->holdEffects[battlerDef])) + RETURN_SCORE_MINUS(10); + + if (IsSemiInvulnerable(battlerDef, move) && moveEffect != EFFECT_SEMI_INVULNERABLE && AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER) + RETURN_SCORE_MINUS(10); + + if (IsTwoTurnNotSemiInvulnerableMove(battlerAtk, move) && CanTargetFaintAi(battlerDef, battlerAtk)) + RETURN_SCORE_MINUS(10); + + // check if negates type + switch (effectiveness) + { + case AI_EFFECTIVENESS_x0: + RETURN_SCORE_MINUS(20); + break; + case AI_EFFECTIVENESS_x0_125: + case AI_EFFECTIVENESS_x0_25: + RETURN_SCORE_MINUS(10); + break; + } + // check non-user target if (!(moveTarget & MOVE_TARGET_USER)) { - // handle negative checks on non-user target - // check powder moves - if (gMovesInfo[move].powderMove && !IsAffectedByPowder(battlerDef, aiData->abilities[battlerDef], aiData->holdEffects[battlerDef])) - { - RETURN_SCORE_MINUS(20); - } - - // check ground immunities - if (moveType == TYPE_GROUND - && !IsBattlerGrounded(battlerDef) - && ((aiData->abilities[battlerDef] == ABILITY_LEVITATE - && DoesBattlerIgnoreAbilityChecks(aiData->abilities[battlerAtk], move)) - || aiData->holdEffects[battlerDef] == HOLD_EFFECT_AIR_BALLOON - || (gStatuses3[battlerDef] & (STATUS3_MAGNET_RISE | STATUS3_TELEKINESIS))) - && move != MOVE_THOUSAND_ARROWS) - { - RETURN_SCORE_MINUS(20); - } - - // check off screen - if (IsSemiInvulnerable(battlerDef, move) && moveEffect != EFFECT_SEMI_INVULNERABLE && AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER) - RETURN_SCORE_MINUS(20); // if target off screen and we go first, don't use move - - if (IsTwoTurnNotSemiInvulnerableMove(battlerAtk, move) && CanTargetFaintAi(battlerDef, battlerAtk)) - RETURN_SCORE_MINUS(10); - - // check if negates type - switch (effectiveness) - { - case AI_EFFECTIVENESS_x0: - RETURN_SCORE_MINUS(20); - break; - case AI_EFFECTIVENESS_x0_125: - case AI_EFFECTIVENESS_x0_25: - RETURN_SCORE_MINUS(10); - break; - } - // target ability checks if (!DoesBattlerIgnoreAbilityChecks(aiData->abilities[battlerAtk], move)) { @@ -859,30 +842,10 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) break; } break; - case ABILITY_VOLT_ABSORB: - case ABILITY_MOTOR_DRIVE: - case ABILITY_LIGHTNING_ROD: - if (moveType == TYPE_ELECTRIC) - RETURN_SCORE_MINUS(20); - break; - case ABILITY_WATER_ABSORB: - case ABILITY_DRY_SKIN: - case ABILITY_STORM_DRAIN: - if (moveType == TYPE_WATER) - RETURN_SCORE_MINUS(20); - break; - case ABILITY_FLASH_FIRE: - if (moveType == TYPE_FIRE) - RETURN_SCORE_MINUS(20); - break; case ABILITY_WONDER_GUARD: if (effectiveness < AI_EFFECTIVENESS_x2) return 0; break; - case ABILITY_SAP_SIPPER: - if (moveType == TYPE_GRASS) - RETURN_SCORE_MINUS(20); - break; case ABILITY_JUSTIFIED: if (moveType == TYPE_DARK && !IS_MOVE_STATUS(move)) RETURN_SCORE_MINUS(10); @@ -892,14 +855,6 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) && (moveType == TYPE_DARK || moveType == TYPE_GHOST || moveType == TYPE_BUG)) RETURN_SCORE_MINUS(10); break; - case ABILITY_SOUNDPROOF: - if (gMovesInfo[move].soundMove) - RETURN_SCORE_MINUS(10); - break; - case ABILITY_BULLETPROOF: - if (gMovesInfo[move].ballisticMove) - RETURN_SCORE_MINUS(10); - break; case ABILITY_DAZZLING: case ABILITY_QUEENLY_MAJESTY: case ABILITY_ARMOR_TAIL: @@ -1105,12 +1060,6 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) ADJUST_SCORE(-1); } break; - case EFFECT_DREAM_EATER: - if (!AI_IsBattlerAsleepOrComatose(battlerDef)) - ADJUST_SCORE(-8); - else if (effectiveness == AI_EFFECTIVENESS_x0) - ADJUST_SCORE(-10); - break; // stat raising effects case EFFECT_ATTACK_UP: case EFFECT_ATTACK_UP_2: @@ -1906,10 +1855,6 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) else if (aiData->hpPercents[battlerAtk] >= 90) ADJUST_SCORE(-8); //No point in healing, but should at least do it if nothing better break; - case EFFECT_SUPER_FANG: - if (aiData->hpPercents[battlerDef] < 50) - ADJUST_SCORE(-4); - break; case EFFECT_RECOIL_IF_MISS: if (aiData->abilities[battlerAtk] != ABILITY_MAGIC_GUARD && AI_DATA->moveAccuracy[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex] < 75) ADJUST_SCORE(-6); @@ -1935,11 +1880,6 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) break; case EFFECT_METRONOME: break; - case EFFECT_ENDEAVOR: - case EFFECT_PAIN_SPLIT: - if (gBattleMons[battlerAtk].hp > (gBattleMons[battlerAtk].hp + gBattleMons[battlerDef].hp) / 2) - ADJUST_SCORE(-10); - break; case EFFECT_CONVERSION_2: //TODO @@ -1965,9 +1905,6 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (gBattleMons[battlerDef].status2 & STATUS2_DESTINY_BOND) ADJUST_SCORE(-10); break; - case EFFECT_FALSE_SWIPE: - // TODO - break; case EFFECT_HEAL_BELL: if (!AnyPartyMemberStatused(battlerAtk, gMovesInfo[move].soundMove) || PartnerHasSameMoveEffectWithoutTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) ADJUST_SCORE(-10); @@ -2050,10 +1987,6 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) || DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) ADJUST_SCORE(-9); break; - case EFFECT_FAIL_IF_NOT_ARG_TYPE: - if (!IS_BATTLER_OF_TYPE(battlerAtk, gMovesInfo[move].argument)) - ADJUST_SCORE(-10); - break; case EFFECT_DEFOG: if (gSideStatuses[GetBattlerSide(battlerDef)] & (SIDE_STATUS_REFLECT | SIDE_STATUS_LIGHTSCREEN | SIDE_STATUS_AURORA_VEIL | SIDE_STATUS_SAFEGUARD | SIDE_STATUS_MIST) @@ -2155,10 +2088,6 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (!HasMagicCoatAffectedMove(battlerDef)) ADJUST_SCORE(-10); break; - case EFFECT_BELCH: - if (ItemId_GetPocket(GetUsedHeldItem(battlerAtk)) != POCKET_BERRIES) - ADJUST_SCORE(-10); // attacker has not consumed a berry - break; case EFFECT_YAWN: if (gStatuses3[battlerDef] & STATUS3_YAWN) ADJUST_SCORE(-10); @@ -2578,10 +2507,6 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (!CanCamouflage(battlerAtk)) ADJUST_SCORE(-10); break; - case EFFECT_LAST_RESORT: - if (!CanUseLastResort(battlerAtk)) - ADJUST_SCORE(-10); - break; case EFFECT_SYNCHRONOISE: //Check holding ring target or is of same type if (aiData->holdEffects[battlerDef] == HOLD_EFFECT_RING_TARGET @@ -2655,10 +2580,6 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) && !BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPDEF)) ADJUST_SCORE(-10); break; - case EFFECT_LOW_KICK: - if (IsDynamaxed(battlerDef)) - ADJUST_SCORE(-10); - break; case EFFECT_UPPER_HAND: if (predictedMove == MOVE_NONE || IS_MOVE_STATUS(predictedMove) || AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_SLOWER || GetMovePriority(battlerDef, move) < 1 || GetMovePriority(battlerDef, move) > 3) // Opponent going first or not using priority move ADJUST_SCORE(-10); @@ -3140,7 +3061,7 @@ static s32 AI_CompareDamagingMoves(u32 battlerAtk, u32 battlerDef, u32 currId) if (moves[i] != MOVE_NONE && gMovesInfo[moves[i]].power) { noOfHits[i] = GetNoOfHitsToKOBattler(battlerAtk, battlerDef, i); - if (noOfHits[i] < leastHits) + if (noOfHits[i] < leastHits && noOfHits[i] != 0) { leastHits = noOfHits[i]; } @@ -4694,7 +4615,12 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score return score; if (gMovesInfo[move].power) - score += AI_CompareDamagingMoves(battlerAtk, battlerDef, AI_THINKING_STRUCT->movesetIndex); + { + if (GetNoOfHitsToKOBattler(battlerAtk, battlerDef, AI_THINKING_STRUCT->movesetIndex) == 0) + ADJUST_SCORE(-20); + else + score += AI_CompareDamagingMoves(battlerAtk, battlerDef, AI_THINKING_STRUCT->movesetIndex); + } // Calculates score based on effects of a move score += AI_CalcMoveScore(battlerAtk, battlerDef, move); diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index f78ee6d674..afbfc095d7 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -366,10 +366,84 @@ static inline s32 LowestRollDmg(s32 dmg) return dmg; } +bool32 IsDamageMoveUsable(u32 move, u32 battlerAtk, u32 battlerDef) +{ + s32 moveType; + struct AiLogicData *aiData = AI_DATA; + u32 battlerDefAbility; + + if (DoesBattlerIgnoreAbilityChecks(aiData->abilities[battlerAtk], move)) + battlerDefAbility = ABILITY_NONE; + else + battlerDefAbility = aiData->abilities[battlerDef]; + + SetTypeBeforeUsingMove(move, battlerAtk); + GET_MOVE_TYPE(move, moveType); + + switch (battlerDefAbility) + { + case ABILITY_VOLT_ABSORB: + case ABILITY_MOTOR_DRIVE: + case ABILITY_LIGHTNING_ROD: + if (moveType == TYPE_ELECTRIC) + return TRUE; + break; + case ABILITY_WATER_ABSORB: + case ABILITY_DRY_SKIN: + case ABILITY_STORM_DRAIN: + if (moveType == TYPE_WATER) + return TRUE; + break; + case ABILITY_FLASH_FIRE: + if (moveType == TYPE_FIRE) + return TRUE; + break; + case ABILITY_SOUNDPROOF: + if (gMovesInfo[move].soundMove) + return TRUE; + break; + case ABILITY_BULLETPROOF: + if (gMovesInfo[move].ballisticMove) + return TRUE; + break; + case ABILITY_SAP_SIPPER: + if (moveType == TYPE_GRASS) + return TRUE; + break; + } + + switch (gMovesInfo[move].effect) + { + case EFFECT_DREAM_EATER: + if (!AI_IsBattlerAsleepOrComatose(battlerDef)) + return TRUE; + break; + case EFFECT_BELCH: + if (ItemId_GetPocket(GetUsedHeldItem(battlerAtk)) != POCKET_BERRIES) + return TRUE; + break; + case EFFECT_LAST_RESORT: + if (!CanUseLastResort(battlerAtk)) + return TRUE; + break; + case EFFECT_LOW_KICK: + if (IsDynamaxed(battlerDef)) + return TRUE; + break; + case EFFECT_FAIL_IF_NOT_ARG_TYPE: + if (!IS_BATTLER_OF_TYPE(battlerAtk, gMovesInfo[move].argument)) + return TRUE; + break; + } + + return FALSE; +} + s32 AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower, u32 weather) { s32 dmg, moveType; uq4_12_t effectivenessMultiplier; + bool32 isDamageMoveUnusable = FALSE; struct AiLogicData *aiData = AI_DATA; SetBattlerData(battlerAtk); @@ -393,8 +467,11 @@ s32 AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectivenes SetTypeBeforeUsingMove(move, battlerAtk); GET_MOVE_TYPE(move, moveType); - effectivenessMultiplier = CalcTypeEffectivenessMultiplier(move, moveType, battlerAtk, battlerDef, aiData->abilities[battlerDef], FALSE); if (gMovesInfo[move].power) + isDamageMoveUnusable = IsDamageMoveUsable(move, battlerAtk, battlerDef); + + effectivenessMultiplier = CalcTypeEffectivenessMultiplier(move, moveType, battlerAtk, battlerDef, aiData->abilities[battlerDef], FALSE); + if (gMovesInfo[move].power && !isDamageMoveUnusable) { s32 critChanceIndex, normalDmg, fixedBasePower, n; diff --git a/test/battle/ai.c b/test/battle/ai.c index 7b9d07429e..b52400acc9 100644 --- a/test/battle/ai.c +++ b/test/battle/ai.c @@ -752,3 +752,28 @@ AI_DOUBLE_BATTLE_TEST("AI will not try to switch for the same pokemon for 2 spot } } } + +AI_SINGLE_BATTLE_TEST("AI will not choose Burn Up if the user lost the Fire typing") +{ + GIVEN { + ASSUME(gMovesInfo[MOVE_BURN_UP].effect == EFFECT_FAIL_IF_NOT_ARG_TYPE); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_CYNDAQUIL) { Moves(MOVE_BURN_UP, MOVE_EXTRASENSORY, MOVE_FLAMETHROWER); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_BURN_UP); } + TURN { EXPECT_MOVE(opponent, MOVE_FLAMETHROWER); } + } +} + +AI_SINGLE_BATTLE_TEST("AI will choose Surf over Thunderbolt and Ice Beam if the opposing mon has Volt Absorb") +{ + GIVEN { + ASSUME(gMovesInfo[MOVE_THUNDERBOLT].type == TYPE_ELECTRIC); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_LANTURN) { Ability(ABILITY_VOLT_ABSORB); }; + OPPONENT(SPECIES_LANTURN) { Moves(MOVE_THUNDERBOLT, MOVE_ICE_BEAM, MOVE_SURF); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_SURF); } + } +}