From f4ff5e6d267b2e1a07b218fca6b31688da034d98 Mon Sep 17 00:00:00 2001 From: Emilia Daelman <48217459+Emiliasky@users.noreply.github.com> Date: Sun, 8 Jun 2025 21:50:34 +0200 Subject: [PATCH] Feature/ai/wide guard quick guard singles (#7086) Co-authored-by: Alex <93446519+AlexOn1ine@users.noreply.github.com> --- include/battle_ai_util.h | 3 ++- src/battle_ai_main.c | 24 ++++++++++++++----- src/battle_ai_util.c | 37 +++++++++++++++++++---------- test/battle/ai/ai_check_viability.c | 25 +++++++++++++++++++ 4 files changed, 69 insertions(+), 20 deletions(-) diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 345212ec47..75a08f5b4a 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -167,7 +167,8 @@ bool32 IsMoveRedirectionPrevented(u32 battlerAtk, u32 move, u32 atkAbility); bool32 IsMoveEncouragedToHit(u32 battlerAtk, u32 battlerDef, u32 move); bool32 IsHazardMove(u32 move); bool32 IsTwoTurnNotSemiInvulnerableMove(u32 battlerAtk, u32 move); -void ProtectChecks(u32 battlerAtk, u32 battlerDef, u32 move, u32 predictedMove, s32 *score); +bool32 IsBattlerDamagedByStatus(u32 battler); +s32 ProtectChecks(u32 battlerAtk, u32 battlerDef, u32 move, u32 predictedMove); bool32 ShouldSetSandstorm(u32 battler, u32 ability, enum ItemHoldEffect holdEffect); bool32 ShouldSetHail(u32 battler, u32 ability, enum ItemHoldEffect holdEffect); bool32 ShouldSetSnow(u32 battler, u32 ability, enum ItemHoldEffect holdEffect); diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 3a7cdb410d..9924dc74c4 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -2202,7 +2202,17 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) switch (protectMethod) { case PROTECT_QUICK_GUARD: + if (GetMovePriority(predictedMove) <= 0) + { + ADJUST_SCORE(-10); + decreased = TRUE; + } case PROTECT_WIDE_GUARD: + if(!(GetBattlerMoveTargetType(battlerAtk, predictedMove) & (MOVE_TARGET_FOES_AND_ALLY | MOVE_TARGET_BOTH))) + { + ADJUST_SCORE(-10); + decreased = TRUE; + } case PROTECT_CRAFTY_SHIELD: if (!isDoubleBattle) { @@ -4288,28 +4298,30 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move) { case PROTECT_QUICK_GUARD: if (predictedMove != MOVE_NONE && GetMovePriority(predictedMove) > 0) - ProtectChecks(battlerAtk, battlerDef, move, predictedMove, &score); + { + ADJUST_SCORE(ProtectChecks(battlerAtk, battlerDef, move, predictedMove)); + } break; case PROTECT_WIDE_GUARD: if (predictedMove != MOVE_NONE && GetBattlerMoveTargetType(battlerDef, predictedMove) & (MOVE_TARGET_FOES_AND_ALLY | MOVE_TARGET_BOTH)) { - ProtectChecks(battlerAtk, battlerDef, move, predictedMove, &score); + ADJUST_SCORE(ProtectChecks(battlerAtk, battlerDef, move, predictedMove)); } else if (isDoubleBattle && GetBattlerMoveTargetType(BATTLE_PARTNER(battlerAtk), aiData->partnerMove) & MOVE_TARGET_FOES_AND_ALLY) { if (aiData->abilities[battlerAtk] != ABILITY_TELEPATHY) - ProtectChecks(battlerAtk, battlerDef, move, predictedMove, &score); + ADJUST_SCORE(ProtectChecks(battlerAtk, battlerDef, move, predictedMove)); } break; case PROTECT_CRAFTY_SHIELD: if (predictedMove != MOVE_NONE && IsBattleMoveStatus(predictedMove) && !(GetBattlerMoveTargetType(battlerDef, predictedMove) & MOVE_TARGET_USER)) - ProtectChecks(battlerAtk, battlerDef, move, predictedMove, &score); + ADJUST_SCORE(ProtectChecks(battlerAtk, battlerDef, move, predictedMove)); break; case PROTECT_MAT_BLOCK: if (gDisableStructs[battlerAtk].isFirstTurn && predictedMove != MOVE_NONE && !IsBattleMoveStatus(predictedMove) && !(GetBattlerMoveTargetType(battlerDef, predictedMove) & MOVE_TARGET_USER)) - ProtectChecks(battlerAtk, battlerDef, move, predictedMove, &score); + ADJUST_SCORE(ProtectChecks(battlerAtk, battlerDef, move, predictedMove)); break; case PROTECT_KINGS_SHIELD: if (aiData->abilities[battlerAtk] == ABILITY_STANCE_CHANGE //Special logic for Aegislash @@ -4321,7 +4333,7 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move) } //fallthrough default: // protect - ProtectChecks(battlerAtk, battlerDef, move, predictedMove, &score); + ADJUST_SCORE(ProtectChecks(battlerAtk, battlerDef, move, predictedMove)); break; } break; diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 5d263e9b56..c9363e344a 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -1893,8 +1893,19 @@ bool32 ShouldSetSnow(u32 battler, u32 ability, enum ItemHoldEffect holdEffect) return FALSE; } -void ProtectChecks(u32 battlerAtk, u32 battlerDef, u32 move, u32 predictedMove, s32 *score) +bool32 IsBattlerDamagedByStatus(u32 battler) { + return gBattleMons[battler].status1 & (STATUS1_BURN | STATUS1_FROSTBITE | STATUS1_POISON | STATUS1_TOXIC_POISON) + || gBattleMons[battler].status2 & (STATUS2_WRAPPED | STATUS2_NIGHTMARE | STATUS2_CURSED) + || gStatuses3[battler] & (STATUS3_PERISH_SONG | STATUS3_LEECHSEED) + || gStatuses4[battler] & (STATUS4_SALT_CURE) + || gSideStatuses[GetBattlerSide(battler)] & (SIDE_STATUS_SEA_OF_FIRE | SIDE_STATUS_DAMAGE_NON_TYPES); +} + +s32 ProtectChecks(u32 battlerAtk, u32 battlerDef, u32 move, u32 predictedMove) +{ + s32 score = 0; + // TODO more sophisticated logic u32 uses = gDisableStructs[battlerAtk].protectUses; @@ -1907,29 +1918,29 @@ void ProtectChecks(u32 battlerAtk, u32 battlerDef, u32 move, u32 predictedMove, if (uses == 0) { if (predictedMove != MOVE_NONE && predictedMove != 0xFFFF && !IsBattleMoveStatus(predictedMove)) - ADJUST_SCORE_PTR(DECENT_EFFECT); + score += DECENT_EFFECT; else if (Random() % 256 < 100) - ADJUST_SCORE_PTR(WEAK_EFFECT); + score += WEAK_EFFECT; } else { if (IsDoubleBattle()) - ADJUST_SCORE_PTR(-(2 * min(uses, 3))); + score -= (2 * min(uses, 3)); else - ADJUST_SCORE_PTR(-(min(uses, 3))); + score -= (min(uses, 3)); } - if (gBattleMons[battlerAtk].status1 & (STATUS1_PSN_ANY | STATUS1_BURN | STATUS1_FROSTBITE) - || gBattleMons[battlerAtk].status2 & (STATUS2_CURSED | STATUS2_INFATUATION) - || gStatuses3[battlerAtk] & (STATUS3_PERISH_SONG | STATUS3_LEECHSEED | STATUS3_YAWN)) + if (IsBattlerDamagedByStatus(battlerAtk)) { - ADJUST_SCORE_PTR(-1); + score -= 1; } - if (gBattleMons[battlerDef].status1 & STATUS1_TOXIC_POISON - || gBattleMons[battlerDef].status2 & (STATUS2_CURSED | STATUS2_INFATUATION) - || gStatuses3[battlerDef] & (STATUS3_PERISH_SONG | STATUS3_LEECHSEED | STATUS3_YAWN)) - ADJUST_SCORE_PTR(DECENT_EFFECT); + if (IsBattlerDamagedByStatus(battlerDef)) + { + score += DECENT_EFFECT; + } + + return score; } // stat stages diff --git a/test/battle/ai/ai_check_viability.c b/test/battle/ai/ai_check_viability.c index f80597f476..0a5a9ff3df 100644 --- a/test/battle/ai/ai_check_viability.c +++ b/test/battle/ai/ai_check_viability.c @@ -1,6 +1,9 @@ +#include "constants/battle_ai.h" +#include "constants/moves.h" #include "global.h" #include "test/battle.h" #include "battle_ai_util.h" +#include "test/test.h" ASSUMPTIONS { @@ -253,3 +256,25 @@ AI_SINGLE_BATTLE_TEST("AI prioritizes Pursuit if it would KO opponent") TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_PURSUIT); SEND_OUT(player, 1); } } } + +AI_SINGLE_BATTLE_TEST("AI uses Quick Guard against Quick Attack when opponent would die of poison") +{ + GIVEN { + PLAYER(SPECIES_ZUBAT) { Moves(MOVE_QUICK_ATTACK); Status1(STATUS1_TOXIC_POISON); } + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT | AI_FLAG_PREDICT_MOVE); + OPPONENT(SPECIES_RATTATA) { Moves(MOVE_QUICK_GUARD, MOVE_TACKLE); } + } WHEN { + TURN { MOVE(player, MOVE_QUICK_ATTACK); EXPECT_MOVE(opponent, MOVE_QUICK_GUARD); } + } +} + +AI_SINGLE_BATTLE_TEST("AI uses Wide Guard against Earthquake when opponent would die of poison") +{ + GIVEN { + PLAYER(SPECIES_ZUBAT) { Moves(MOVE_EARTHQUAKE); Status1(STATUS1_TOXIC_POISON); } + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT | AI_FLAG_PREDICT_MOVE); + OPPONENT(SPECIES_RATTATA) { Moves(MOVE_WIDE_GUARD, MOVE_TACKLE); } + } WHEN { + TURN { MOVE(player, MOVE_EARTHQUAKE); EXPECT_MOVE(opponent, MOVE_WIDE_GUARD); } + } +}