From ccda2308a3555c0689b402cb965f05295436fb4e Mon Sep 17 00:00:00 2001 From: hedara90 <90hedara@gmail.com> Date: Fri, 2 May 2025 17:30:09 +0200 Subject: [PATCH] Add AI_FLAG_PP_STALL_PREVENTION (#6743) Co-authored-by: Hedara --- include/battle.h | 1 + include/battle_util.h | 1 + include/config/ai.h | 5 +++ include/constants/battle_ai.h | 5 ++- include/random.h | 1 + src/battle_ai_main.c | 56 ++++++++++++++++++++++++- src/battle_script_commands.c | 2 + src/battle_util.c | 25 +++++++++++ test/battle/ai/ai_pp_stall_prevention.c | 18 ++++++++ test/battle/ai/ai_switching.c | 20 --------- 10 files changed, 111 insertions(+), 23 deletions(-) create mode 100644 test/battle/ai/ai_pp_stall_prevention.c diff --git a/include/battle.h b/include/battle.h index 0d3cc543f0..6987a57672 100644 --- a/include/battle.h +++ b/include/battle.h @@ -816,6 +816,7 @@ struct BattleStruct struct AiBattleData { s32 finalScore[MAX_BATTLERS_COUNT][MAX_BATTLERS_COUNT][MAX_MON_MOVES]; // AI, target, moves to make debugging easier + u8 playerStallMons[PARTY_SIZE]; u8 chosenMoveIndex[MAX_BATTLERS_COUNT]; u8 chosenTarget[MAX_BATTLERS_COUNT]; u8 actionFlee:1; diff --git a/include/battle_util.h b/include/battle_util.h index 6f5bfc5418..84136599db 100644 --- a/include/battle_util.h +++ b/include/battle_util.h @@ -377,5 +377,6 @@ bool32 HasWeatherEffect(void); u32 RestoreWhiteHerbStats(u32 battler); bool32 IsFutureSightAttackerInParty(u32 battlerAtk, u32 battlerDef); bool32 HadMoreThanHalfHpNowDoesnt(u32 battler); +void UpdateStallMons(void); #endif // GUARD_BATTLE_UTIL_H diff --git a/include/config/ai.h b/include/config/ai.h index fdc7a789e0..de49a2248f 100644 --- a/include/config/ai.h +++ b/include/config/ai.h @@ -60,4 +60,9 @@ // AI prediction chances #define PREDICT_SWITCH_CHANCE 50 +// AI PP Stall detection chance per roll +#define PP_STALL_DISREGARD_MOVE_PERCENTAGE 50 +// Score reduction if any roll for PP stall detection passes +#define PP_STALL_SCORE_REDUCTION 20 + #endif // GUARD_CONFIG_AI_H diff --git a/include/constants/battle_ai.h b/include/constants/battle_ai.h index dfc023676a..c8dd9bd1f9 100644 --- a/include/constants/battle_ai.h +++ b/include/constants/battle_ai.h @@ -30,12 +30,13 @@ #define AI_FLAG_PREFER_HIGHEST_DAMAGE_MOVE (1 << 22) // AI adds score to highest damage move regardless of accuracy or secondary effect #define AI_FLAG_PREDICT_SWITCH (1 << 23) // AI will predict the player's switches and switchins based on how it would handle the situation. Recommend using AI_FLAG_OMNISCIENT #define AI_FLAG_PREDICT_INCOMING_MON (1 << 24) // AI will score against the predicting incoming mon if it predicts the player to switch. Requires AI_FLAG_PREDICT_SWITCH +#define AI_FLAG_PP_STALL_PREVENTION (1 << 25) // AI keeps track of the player's switches where the incoming mon is immune to the chosen move -#define AI_FLAG_COUNT 25 +#define AI_FLAG_COUNT 26 // The following options are enough to have a basic/smart trainer. Any other addtion could make the trainer worse/better depending on the flag #define AI_FLAG_BASIC_TRAINER (AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY) -#define AI_FLAG_SMART_TRAINER (AI_FLAG_BASIC_TRAINER | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_WEIGH_ABILITY_PREDICTION) +#define AI_FLAG_SMART_TRAINER (AI_FLAG_BASIC_TRAINER | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_WEIGH_ABILITY_PREDICTION | AI_FLAG_PP_STALL_PREVENTION) #define AI_FLAG_PREDICTION (AI_FLAG_PREDICT_SWITCH | AI_FLAG_PREDICT_INCOMING_MON) // 'other' ai logic flags diff --git a/include/random.h b/include/random.h index b5ab0ac9bf..639a85164b 100644 --- a/include/random.h +++ b/include/random.h @@ -187,6 +187,7 @@ enum RandomTag RNG_AI_SWITCH_TRAPPER, RNG_AI_SWITCH_FREE_TURN, RNG_AI_SWITCH_ALL_MOVES_BAD, + RNG_AI_PP_STALL_DISREGARD_MOVE, RNG_SHELL_SIDE_ARM, RNG_RANDOM_TARGET, RNG_AI_PREDICT_ABILITY, diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 8acfe60858..490f1b5786 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -58,6 +58,7 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score); static s32 AI_PowerfulStatus(u32 battlerAtk, u32 battlerDef, u32 move, s32 score); static s32 AI_DynamicFunc(u32 battlerAtk, u32 battlerDef, u32 move, s32 score); static s32 AI_PredictSwitch(u32 battlerAtk, u32 battlerDef, u32 move, s32 score); +static s32 AI_CheckPpStall(u32 battlerAtk, u32 battlerDef, u32 move, s32 score); static s32 (*const sBattleAiFuncTable[])(u32, u32, u32, s32) = { @@ -86,7 +87,7 @@ static s32 (*const sBattleAiFuncTable[])(u32, u32, u32, s32) = [22] = NULL, // Unused [23] = AI_PredictSwitch, // AI_FLAG_PREDICT_SWITCH [24] = NULL, // Unused - [25] = NULL, // Unused + [25] = AI_CheckPpStall, // AI_FLAG_PP_STALL_PREVENTION [26] = NULL, // Unused [27] = NULL, // Unused [28] = AI_DynamicFunc, // AI_FLAG_DYNAMIC_FUNC @@ -554,6 +555,52 @@ void SetAiLogicDataForTurn(struct AiLogicData *aiData) AI_DATA->aiCalcInProgress = FALSE; } +u32 GetPartyMonAbility(struct Pokemon *mon) +{ + // Doesn't have any special handling yet + u32 species = GetMonData(mon, MON_DATA_SPECIES); + u32 ability = gSpeciesInfo[species].abilities[GetMonData(mon, MON_DATA_ABILITY_NUM)]; + return ability; +} + +static u32 PpStallReduction(u32 move, u32 battlerAtk) +{ + if (move == MOVE_NONE) + return 0; + u32 tempBattleMonIndex = 0; + u32 totalStallValue = 0; + u32 returnValue = 0; + struct BattlePokemon backupBattleMon; + memcpy(&backupBattleMon, &gBattleMons[tempBattleMonIndex], sizeof(struct BattlePokemon)); + for (u32 partyIndex = 0; partyIndex < PARTY_SIZE; partyIndex++) + { + u32 currentStallValue = gAiBattleData->playerStallMons[partyIndex]; + if (currentStallValue == 0 || GetMonData(&gPlayerParty[partyIndex], MON_DATA_HP) == 0) + continue; + PokemonToBattleMon(&gPlayerParty[partyIndex], &gBattleMons[tempBattleMonIndex]); + u32 species = GetMonData(&gPlayerParty[partyIndex], MON_DATA_SPECIES); + u32 abilityAtk = ABILITY_NONE; + u32 abilityDef = GetPartyMonAbility(&gPlayerParty[partyIndex]); + u32 moveType = GetBattleMoveType(move); // Probably doesn't handle dynamic types right now + if (CanAbilityAbsorbMove(battlerAtk, tempBattleMonIndex, abilityDef, move, moveType, ABILITY_CHECK_TRIGGER) + || CanAbilityBlockMove(battlerAtk, tempBattleMonIndex, abilityAtk, abilityDef, move, ABILITY_CHECK_TRIGGER) + || (CalcPartyMonTypeEffectivenessMultiplier(move, species, abilityDef) == 0)) + { + totalStallValue += currentStallValue; + } + } + + for (u32 i = 0; returnValue == 0 && i < totalStallValue; i++) + { + if (RandomPercentage(RNG_AI_PP_STALL_DISREGARD_MOVE, (100 - PP_STALL_DISREGARD_MOVE_PERCENTAGE))) + returnValue = PP_STALL_SCORE_REDUCTION; + } + + memcpy(&gBattleMons[tempBattleMonIndex], &backupBattleMon, sizeof(struct BattlePokemon)); + + return returnValue; +} + static u32 ChooseMoveOrAction_Singles(u32 battlerAi) { u8 currentMoveArray[MAX_MON_MOVES]; @@ -5637,6 +5684,13 @@ static s32 AI_PredictSwitch(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) return score; } +static s32 AI_CheckPpStall(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) +{ + if (GetBattlerSide(battlerAtk) == B_SIDE_OPPONENT) + score -= PpStallReduction(move, battlerAtk); + return score; +} + static void AI_Flee(void) { AI_THINKING_STRUCT->aiAction |= (AI_ACTION_DONE | AI_ACTION_FLEE | AI_ACTION_DO_NOT_ATTACK); diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 914ffe7fc1..d70aad29f3 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -6841,6 +6841,8 @@ static void Cmd_moveend(void) gBattleScripting.moveendState++; break; case MOVEEND_UPDATE_LAST_MOVES: + if (GetBattlerSide(gBattlerAttacker) == B_SIDE_OPPONENT) + UpdateStallMons(); if ((gBattleStruct->moveResultFlags[gBattlerTarget] & (MOVE_RESULT_FAILED | MOVE_RESULT_DOESNT_AFFECT_FOE)) || (gBattleMons[gBattlerAttacker].status2 & (STATUS2_FLINCHED)) || gProtectStructs[gBattlerAttacker].nonVolatileStatusImmobility) diff --git a/src/battle_util.c b/src/battle_util.c index 43d507707c..589b709f4e 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -11400,3 +11400,28 @@ static bool32 IsAnyTargetAffected(u32 battlerAtk) } return FALSE; } + +void UpdateStallMons(void) +{ + if (IsBattlerTurnDamaged(gBattlerTarget) || IsBattlerProtected(gBattlerAttacker, gBattlerTarget, gCurrentMove) || gMovesInfo[gCurrentMove].category == DAMAGE_CATEGORY_STATUS) + return; + if (!IsDoubleBattle() || gMovesInfo[gCurrentMove].target == MOVE_TARGET_SELECTED) + { + u32 moveType = GetBattleMoveType(gCurrentMove); // Probably doesn't handle dynamic move types right now + u32 abilityAtk = GetBattlerAbility(gBattlerAttacker); + u32 abilityDef = GetBattlerAbility(gBattlerTarget); + if (CanAbilityAbsorbMove(gBattlerAttacker, gBattlerTarget, abilityDef, gCurrentMove, moveType, ABILITY_CHECK_TRIGGER)) + { + gAiBattleData->playerStallMons[gBattlerPartyIndexes[gBattlerTarget]]++; + } + else if (CanAbilityBlockMove(gBattlerAttacker, gBattlerTarget, abilityAtk, abilityDef, gCurrentMove, ABILITY_CHECK_TRIGGER)) + { + gAiBattleData->playerStallMons[gBattlerPartyIndexes[gBattlerTarget]]++; + } + else if (AI_GetMoveEffectiveness(gCurrentMove, gBattlerAttacker, gBattlerTarget) == 0) + { + gAiBattleData->playerStallMons[gBattlerPartyIndexes[gBattlerTarget]]++; + } + } + // Handling for moves that target multiple opponents in doubles not handled currently +} diff --git a/test/battle/ai/ai_pp_stall_prevention.c b/test/battle/ai/ai_pp_stall_prevention.c new file mode 100644 index 0000000000..1c7aa8a49d --- /dev/null +++ b/test/battle/ai/ai_pp_stall_prevention.c @@ -0,0 +1,18 @@ +#include "global.h" +#include "test/battle.h" +#include "battle_ai_util.h" + +AI_SINGLE_BATTLE_TEST("AI_FLAG_PP_STALL_PREVENTION: AI will stop using moves that has hit into immunities due to switches sometimes") +{ + PASSES_RANDOMLY(PP_STALL_DISREGARD_MOVE_PERCENTAGE, 100, RNG_AI_PP_STALL_DISREGARD_MOVE); + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_PP_STALL_PREVENTION); + PLAYER(SPECIES_RATICATE); + PLAYER(SPECIES_GENGAR); + OPPONENT(SPECIES_KARTANA) { Moves(MOVE_SHADOW_CLAW, MOVE_SACRED_SWORD, MOVE_ROCK_SLIDE); } + } WHEN { + TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_SACRED_SWORD); } + TURN { SWITCH(player, 0); EXPECT_MOVE(opponent, MOVE_SHADOW_CLAW); } + TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_SACRED_SWORD); } + } +} diff --git a/test/battle/ai/ai_switching.c b/test/battle/ai/ai_switching.c index e8921cd3ac..882016e66d 100644 --- a/test/battle/ai/ai_switching.c +++ b/test/battle/ai/ai_switching.c @@ -1,26 +1,6 @@ #include "global.h" #include "test/battle.h" -AI_SINGLE_BATTLE_TEST("AI gets baited by Protect Switch tactics") // This behavior is to be fixed. -{ - GIVEN { - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING); - PLAYER(SPECIES_STUNFISK); - PLAYER(SPECIES_PELIPPER); - OPPONENT(SPECIES_DARKRAI) { Moves(MOVE_TACKLE, MOVE_PECK, MOVE_EARTHQUAKE, MOVE_THUNDERBOLT); } - OPPONENT(SPECIES_SCIZOR) { Moves(MOVE_HYPER_BEAM, MOVE_FACADE, MOVE_GIGA_IMPACT, MOVE_EXTREME_SPEED); } - } WHEN { - - TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } // E-quake - TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } // E-quake - TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_THUNDERBOLT); } // T-Bolt - TURN { SWITCH(player, 0); EXPECT_MOVE(opponent, MOVE_THUNDERBOLT); } // T-Bolt - TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } // E-quake - TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE);} // E-quake - TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_THUNDERBOLT); } // T-Bolt - } -} - // General switching behaviour AI_SINGLE_BATTLE_TEST("AI switches if Perish Song is about to kill") {