diff --git a/include/battle_ai_main.h b/include/battle_ai_main.h index 8bb2293e9b66..5b6807ab3764 100644 --- a/include/battle_ai_main.h +++ b/include/battle_ai_main.h @@ -1,21 +1,38 @@ #ifndef GUARD_BATTLE_AI_MAIN_H #define GUARD_BATTLE_AI_MAIN_H -// return values for BattleAI_ChooseMoveOrAction +// return vals for BattleAI_ChooseMoveOrAction // 0 - 3 are move idx #define AI_CHOICE_FLEE 4 #define AI_CHOICE_WATCH 5 #define AI_CHOICE_SWITCH 7 +#include "test_runner.h" + +// Logs for debugging AI tests. +#define SET_SCORE(battler, movesetIndex, val) \ + do \ + { \ + TestRunner_Battle_AISetScore(__FILE__, __LINE__, battler, movesetIndex, val); \ + AI_THINKING_STRUCT->score[movesetIndex] = val; \ + } while (0) \ + +#define ADJUST_SCORE(val) \ + do \ + { \ + TestRunner_Battle_AIAdjustScore(__FILE__, __LINE__, sBattler_AI, AI_THINKING_STRUCT->movesetIndex, val); \ + score += val; \ + } while (0) \ + #define RETURN_SCORE_PLUS(val) \ { \ - score += val; \ + ADJUST_SCORE(val); \ return score; \ } #define RETURN_SCORE_MINUS(val) \ { \ - score -= val; \ + ADJUST_SCORE(-val); \ return score; \ } diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 8de2462d90c3..6a51963ae3bf 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -36,7 +36,6 @@ bool32 CanTargetMoveFaintAi(u32 move, u32 battlerDef, u32 battlerAtk, u32 nHits) bool32 CanTargetFaintAiWithMod(u32 battlerDef, u32 battlerAtk, s32 hpMod, s32 dmgMod); s32 AI_DecideKnownAbilityForTurn(u32 battlerId); u32 AI_DecideHoldEffectForTurn(u32 battlerId); -u32 AI_GetMoveAccuracy(u32 battlerAtk, u32 battlerDef, u32 move); bool32 DoesBattlerIgnoreAbilityChecks(u32 atkAbility, u32 move); u32 AI_GetWeather(struct AiLogicData *aiData); bool32 CanAIFaintTarget(u32 battlerAtk, u32 battlerDef, u32 numHits); @@ -85,7 +84,7 @@ bool32 ShouldLowerEvasion(u32 battlerAtk, u32 battlerDef, u32 defAbility); // move checks bool32 IsAffectedByPowder(u32 battler, u32 ability, u32 holdEffect); bool32 MovesWithSplitUnusable(u32 attacker, u32 target, u32 split); -u32 AI_WhichMoveBetter(u32 move1, u32 move2, u32 battlerAtk, u32 battlerDef); +u32 AI_WhichMoveBetter(u32 move1, u32 move2, u32 battlerAtk, u32 battlerDef, s32 noOfHitsToKo); s32 AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower); s32 AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower, u32 weather); bool32 AI_IsDamagedByRecoil(u32 battler); @@ -169,7 +168,7 @@ bool32 IsTargetingPartner(u32 battlerAtk, u32 battlerDef); bool32 DoesPartnerHaveSameMoveEffect(u32 battlerAtkPartner, u32 battlerDef, u32 move, u32 partnerMove); bool32 PartnerHasSameMoveEffectWithoutTarget(u32 battlerAtkPartner, u32 move, u32 partnerMove); bool32 PartnerMoveEffectIsStatusSameTarget(u32 battlerAtkPartner, u32 battlerDef, u32 partnerMove); -bool32 PartnerMoveEffectIsWeather(u32 battlerAtkPartner, u32 partnerMove); +bool32 IsMoveEffectWeather(u32 move); bool32 PartnerMoveEffectIsTerrain(u32 battlerAtkPartner, u32 partnerMove); bool32 PartnerMoveIs(u32 battlerAtkPartner, u32 partnerMove, u32 moveCheck); bool32 PartnerMoveIsSameAsAttacker(u32 battlerAtkPartner, u32 battlerDef, u32 move, u32 partnerMove); diff --git a/include/battle_util.h b/include/battle_util.h index 9cdd41cab58b..d685830d9a9e 100644 --- a/include/battle_util.h +++ b/include/battle_util.h @@ -147,6 +147,7 @@ bool32 TryChangeBattleWeather(u32 battler, u32 weatherEnumId, bool32 viaAbility) u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 moveArg); bool32 TryPrimalReversion(u32 battler); bool32 IsNeutralizingGasOnField(void); +bool32 IsMoldBreakerTypeAbility(u32 ability); u32 GetBattlerAbility(u32 battler); u32 IsAbilityOnSide(u32 battler, u32 ability); u32 IsAbilityOnOpposingSide(u32 battler, u32 ability); diff --git a/include/constants/battle_ai.h b/include/constants/battle_ai.h index 32b7f7d10c3c..39d0640de13a 100644 --- a/include/constants/battle_ai.h +++ b/include/constants/battle_ai.h @@ -24,14 +24,6 @@ #define AI_EFFECTIVENESS_x0_125 1 #define AI_EFFECTIVENESS_x0 0 -// ai weather -#define AI_WEATHER_NONE 0 -#define AI_WEATHER_SUN 1 -#define AI_WEATHER_RAIN 2 -#define AI_WEATHER_SANDSTORM 3 -#define AI_WEATHER_HAIL 4 -#define AI_WEATHER_SNOW 5 - // get_how_powerful_move_is #define MOVE_POWER_OTHER 0 #define MOVE_POWER_BEST 1 @@ -67,4 +59,6 @@ #define AI_FLAG_SAFARI (1 << 30) #define AI_FLAG_FIRST_BATTLE (1 << 31) +#define AI_SCORE_DEFAULT 100 // Default score for all AI moves. + #endif // GUARD_CONSTANTS_BATTLE_AI_H diff --git a/include/test/battle.h b/include/test/battle.h index 1e264f95bdf1..2314e0961c8d 100644 --- a/include/test/battle.h +++ b/include/test/battle.h @@ -199,6 +199,17 @@ * - Instead of player and opponent there is playerLeft, playerRight, * opponentLeft, and opponentRight. * + * AI_SINGLE_BATTLE_TEST(name, results...) and AI_DOUBLE_BATTLE_TEST(name, results...) + * Define battles where opponent mons are controlled by AI, the same that runs + * when battling regular Trainers. The flags for AI should be specified by + * the AI_FLAGS command. + * The rules remain the same as with the SINGLE and DOUBLE battle tests + * with some differences: + * - opponent's action is specified by the EXPECT_MOVE(s) / EXPECT_SEND_OUT / EXPECT_SWITCH commands + * - we don't control what opponent actually does, instead we make sure the opponent does what we expect it to do + * - we still control the player's action the same way + * - apart from the EXPECTED commands, there's also a new SCORE_ and SCORE__VAL commands + * * KNOWN_FAILING * Marks a test as not passing due to a bug. If there is an issue number * associated with the bug it should be included in a comment. If the @@ -289,6 +300,11 @@ * Note if Moves is specified then MOVE will not automatically add moves * to the moveset. * + * AI_FLAGS + * Specifies which AI flags are run during the test. Has use only for AI tests. + * The most common combination is AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT) + * which is the general 'smart' AI. + * * WHEN * Contains the choices that battlers make during the battle. * @@ -470,6 +486,7 @@ #include "recorded_battle.h" #include "util.h" #include "constants/abilities.h" +#include "constants/battle_ai.h" #include "constants/battle_anim.h" #include "constants/battle_move_effects.h" #include "constants/hold_effects.h" @@ -483,8 +500,9 @@ #define BATTLE_TEST_STACK_SIZE 1024 #define MAX_TURNS 16 #define MAX_QUEUED_EVENTS 25 +#define MAX_EXPECTED_ACTIONS 10 -enum { BATTLE_TEST_SINGLES, BATTLE_TEST_DOUBLES, BATTLE_TEST_WILD }; +enum { BATTLE_TEST_SINGLES, BATTLE_TEST_DOUBLES, BATTLE_TEST_WILD, BATTLE_TEST_AI_SINGLES, BATTLE_TEST_AI_DOUBLES }; typedef void (*SingleBattleTestFunction)(void *, u32, struct BattlePokemon *, struct BattlePokemon *); typedef void (*DoubleBattleTestFunction)(void *, u32, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *); @@ -584,6 +602,42 @@ struct BattlerTurn struct TurnRNG rng; }; +struct ExpectedAIAction +{ + u16 sourceLine; + u8 type:4; // which action + u8 moveSlots:4; // Expected move(s) to be chosen or not, marked as bits. + u8 target:4; // move target or id of mon which gets sent out + u8 explicitTarget:1; // For double battles, if it's set it requires the move to hit a specific target, otherwise any target is fine. + u8 pass:1; // No matter what AI does, it always passes. + u8 notMove:1; // We're expecting AI to choose any move EXCEPT the specified one. + u8 actionSet:1; // Action was set and is expected to happen. Set only for battlers controlled by AI. +}; + +#define MAX_AI_SCORE_COMPARISION_PER_TURN 4 +#define MAX_AI_LOG_LINES 10 + +struct ExpectedAiScore +{ + // We can compare AI's move score to a value or to another move's score. + u8 moveSlot1:2; + u8 moveSlot2:2; + u8 target:2; + s8 value; // value + u8 cmp:3; // Uses battle script command's CMP_ macros + u8 toValue:1; // compare to value, not to move + u8 set:1; + u16 sourceLine; +}; + +struct AILogLine +{ + const char *file; + u16 line:15; + u16 set:1; // Whether score was set, or added/subtracted + s16 score; +}; + struct BattleTestData { u8 stack[BATTLE_TEST_STACK_SIZE]; @@ -606,6 +660,8 @@ struct BattleTestData u8 turns; u8 actionBattlers; u8 moveBattlers; + bool8 hasAI:1; + bool8 logAI:1; struct RecordedBattleSave recordedBattle; u8 battleRecordTypes[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE]; @@ -619,14 +675,20 @@ struct BattleTestData u8 queueGroupStart; u8 queuedEvent; struct QueuedEvent queuedEvents[MAX_QUEUED_EVENTS]; + u8 expectedAiActionIndex[MAX_BATTLERS_COUNT]; + u8 aiActionsPlayed[MAX_BATTLERS_COUNT]; + struct ExpectedAIAction expectedAiActions[MAX_BATTLERS_COUNT][MAX_EXPECTED_ACTIONS]; + struct ExpectedAiScore expectedAiScores[MAX_BATTLERS_COUNT][MAX_TURNS][MAX_AI_SCORE_COMPARISION_PER_TURN]; // Max 4 comparisions per turn + struct AILogLine aiLogLines[MAX_BATTLERS_COUNT][MAX_MON_MOVES][MAX_AI_LOG_LINES]; + u8 aiLogPrintedForMove[MAX_BATTLERS_COUNT]; // Marks ai score log as printed for move, so the same log isn't displayed multiple times. }; struct BattleTestRunnerState { u8 battlersCount; - u8 parametersCount; // Valid only in BattleTest_Setup. - u8 parameters; - u8 runParameter; + u16 parametersCount; // Valid only in BattleTest_Setup. + u16 parameters; + u16 runParameter; u16 rngTag; u16 rngTrialOffset; u16 trials; @@ -682,7 +744,7 @@ extern struct BattleTestRunnerState *gBattleTestRunnerState; TO_DO; \ } -#define SINGLE_BATTLE_TEST(_name, ...) \ +#define BATTLE_TEST_ARGS_SINGLE(_name, _type, ...) \ struct CAT(Result, __LINE__) { MEMBERS(__VA_ARGS__) }; \ static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *, u32, struct BattlePokemon *, struct BattlePokemon *); \ __attribute__((section(".tests"))) static const struct Test CAT(sTest, __LINE__) = \ @@ -692,7 +754,7 @@ extern struct BattleTestRunnerState *gBattleTestRunnerState; .runner = &gBattleTestRunner, \ .data = (void *)&(const struct BattleTest) \ { \ - .type = BATTLE_TEST_SINGLES, \ + .type = _type, \ .sourceLine = __LINE__, \ .function = { .singles = (SingleBattleTestFunction)CAT(Test, __LINE__) }, \ .resultsSize = sizeof(struct CAT(Result, __LINE__)), \ @@ -700,25 +762,7 @@ extern struct BattleTestRunnerState *gBattleTestRunnerState; }; \ static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *results, u32 i, struct BattlePokemon *player, struct BattlePokemon *opponent) -#define WILD_BATTLE_TEST(_name, ...) \ - struct CAT(Result, __LINE__) { MEMBERS(__VA_ARGS__) }; \ - static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *, u32, struct BattlePokemon *, struct BattlePokemon *); \ - __attribute__((section(".tests"))) static const struct Test CAT(sTest, __LINE__) = \ - { \ - .name = _name, \ - .filename = __FILE__, \ - .runner = &gBattleTestRunner, \ - .data = (void *)&(const struct BattleTest) \ - { \ - .type = BATTLE_TEST_WILD, \ - .sourceLine = __LINE__, \ - .function = { .singles = (SingleBattleTestFunction)CAT(Test, __LINE__) }, \ - .resultsSize = sizeof(struct CAT(Result, __LINE__)), \ - }, \ - }; \ - static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *results, u32 i, struct BattlePokemon *player, struct BattlePokemon *opponent) - -#define DOUBLE_BATTLE_TEST(_name, ...) \ +#define BATTLE_TEST_ARGS_DOUBLE(_name, _type, ...) \ struct CAT(Result, __LINE__) { MEMBERS(__VA_ARGS__) }; \ static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *, u32, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *); \ __attribute__((section(".tests"))) static const struct Test CAT(sTest, __LINE__) = \ @@ -728,7 +772,7 @@ extern struct BattleTestRunnerState *gBattleTestRunnerState; .runner = &gBattleTestRunner, \ .data = (void *)&(const struct BattleTest) \ { \ - .type = BATTLE_TEST_DOUBLES, \ + .type = _type, \ .sourceLine = __LINE__, \ .function = { .doubles = (DoubleBattleTestFunction)CAT(Test, __LINE__) }, \ .resultsSize = sizeof(struct CAT(Result, __LINE__)), \ @@ -736,6 +780,14 @@ extern struct BattleTestRunnerState *gBattleTestRunnerState; }; \ static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *results, u32 i, struct BattlePokemon *playerLeft, struct BattlePokemon *opponentLeft, struct BattlePokemon *playerRight, struct BattlePokemon *opponentRight) + +#define SINGLE_BATTLE_TEST(_name, ...) BATTLE_TEST_ARGS_SINGLE(_name, BATTLE_TEST_SINGLES, __VA_ARGS__) +#define WILD_BATTLE_TEST(_name, ...) BATTLE_TEST_ARGS_SINGLE(_name, BATTLE_TEST_WILD, __VA_ARGS__) +#define AI_SINGLE_BATTLE_TEST(_name, ...) BATTLE_TEST_ARGS_SINGLE(_name, BATTLE_TEST_AI_SINGLES, __VA_ARGS__) + +#define DOUBLE_BATTLE_TEST(_name, ...) BATTLE_TEST_ARGS_DOUBLE(_name, BATTLE_TEST_DOUBLES, __VA_ARGS__) +#define AI_DOUBLE_BATTLE_TEST(_name, ...) BATTLE_TEST_ARGS_DOUBLE(_name, BATTLE_TEST_AI_DOUBLES, __VA_ARGS__) + /* Parametrize */ #undef PARAMETRIZE // Override test/test.h's implementation. @@ -763,6 +815,8 @@ struct moveWithPP { #define GIVEN for (; gBattleTestRunnerState->runGiven; gBattleTestRunnerState->runGiven = FALSE) #define RNGSeed(seed) RNGSeed_(__LINE__, seed) +#define AI_FLAGS(flags) AIFlags_(__LINE__, flags) +#define AI_LOG AILogScores(__LINE__) #define PLAYER(species) for (OpenPokemon(__LINE__, B_SIDE_PLAYER, species); gBattleTestRunnerState->data.currentMon; ClosePokemon(__LINE__)) #define OPPONENT(species) for (OpenPokemon(__LINE__, B_SIDE_OPPONENT, species); gBattleTestRunnerState->data.currentMon; ClosePokemon(__LINE__)) @@ -779,7 +833,7 @@ struct moveWithPP { #define SpDefense(spDefense) SpDefense_(__LINE__, spDefense) #define Speed(speed) Speed_(__LINE__, speed) #define Item(item) Item_(__LINE__, item) -#define Moves(move1, ...) Moves_(__LINE__, (const u16 [MAX_MON_MOVES]) { move1, __VA_ARGS__ }) +#define Moves(move1, ...) do { u16 moves_[MAX_MON_MOVES] = {move1, __VA_ARGS__}; Moves_(__LINE__, moves_); } while(0) #define MovesWithPP(movewithpp1, ...) MovesWithPP_(__LINE__, (struct moveWithPP[MAX_MON_MOVES]) {movewithpp1, __VA_ARGS__}) #define Friendship(friendship) Friendship_(__LINE__, friendship) #define Status1(status1) Status1_(__LINE__, status1) @@ -789,6 +843,8 @@ void OpenPokemon(u32 sourceLine, u32 side, u32 species); void ClosePokemon(u32 sourceLine); void RNGSeed_(u32 sourceLine, u32 seed); +void AIFlags_(u32 sourceLine, u32 flags); +void AILogScores(u32 sourceLine); void Gender_(u32 sourceLine, u32 gender); void Nature_(u32 sourceLine, u32 nature); void Ability_(u32 sourceLine, u32 ability); @@ -801,12 +857,28 @@ void SpAttack_(u32 sourceLine, u32 spAttack); void SpDefense_(u32 sourceLine, u32 spDefense); void Speed_(u32 sourceLine, u32 speed); void Item_(u32 sourceLine, u32 item); -void Moves_(u32 sourceLine, const u16 moves[MAX_MON_MOVES]); +void Moves_(u32 sourceLine, u16 moves[MAX_MON_MOVES]); void MovesWithPP_(u32 sourceLine, struct moveWithPP moveWithPP[MAX_MON_MOVES]); void Friendship_(u32 sourceLine, u32 friendship); void Status1_(u32 sourceLine, u32 status1); void OTName_(u32 sourceLine, const u8 *otName); +// Created for easy use of EXPECT_MOVES, so the user can provide 1, 2, 3 or 4 moves for AI which can pass the test. +struct FourMoves +{ + u16 moves[MAX_MON_MOVES]; +}; + +struct TestAIScoreStruct +{ + u32 move1; + bool8 explicitMove1; + u32 valueOrMoveId2; + bool8 explicitValueOrMoveId2; + struct BattlePokemon *target; + bool8 explicitTarget; +}; + #define PLAYER_PARTY (gBattleTestRunnerState->data.recordedBattle.playerParty) #define OPPONENT_PARTY (gBattleTestRunnerState->data.recordedBattle.opponentParty) @@ -819,6 +891,22 @@ enum { TURN_CLOSED, TURN_OPEN, TURN_CLOSING }; #define TURN for (OpenTurn(__LINE__); gBattleTestRunnerState->data.turnState == TURN_OPEN; CloseTurn(__LINE__)) #define MOVE(battler, ...) Move(__LINE__, battler, (struct MoveContext) { APPEND_TRUE(__VA_ARGS__) }) + +#define EXPECT_MOVE(battler, ...) ExpectMove(__LINE__, battler, (struct MoveContext) { APPEND_TRUE(__VA_ARGS__) }) +#define NOT_EXPECT_MOVE(battler, _move) ExpectMove(__LINE__, battler, (struct MoveContext) { .move = _move, .explicitMove = TRUE, .notExpected = TRUE, .explicitNotExpected = TRUE, }) +#define EXPECT_MOVES(battler, ...) ExpectMoves(__LINE__, battler, FALSE, (struct FourMoves) { __VA_ARGS__ }) +#define NOT_EXPECT_MOVES(battler, ...) ExpectMoves(__LINE__, battler, TRUE, (struct FourMoves) { __VA_ARGS__ }) +#define EXPECT_SEND_OUT(battler, partyIndex) ExpectSendOut(__LINE__, battler, partyIndex) +#define EXPECT_SWITCH(battler, partyIndex) ExpectSwitch(__LINE__, battler, partyIndex) +#define SCORE_EQ(battler, ...) Score(__LINE__, battler, CMP_EQUAL, FALSE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } ) +#define SCORE_NE(battler, ...) Score(__LINE__, battler, CMP_NOT_EQUAL, FALSE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } ) +#define SCORE_GT(battler, ...) Score(__LINE__, battler, CMP_GREATER_THAN, FALSE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } ) +#define SCORE_LT(battler, ...) Score(__LINE__, battler, CMP_LESS_THAN, FALSE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } ) +#define SCORE_EQ_VAL(battler, ...) Score(__LINE__, battler, CMP_EQUAL, TRUE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } ) +#define SCORE_NE_VAL(battler, ...) Score(__LINE__, battler, CMP_NOT_EQUAL, TRUE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } ) +#define SCORE_GT_VAL(battler, ...) Score(__LINE__, battler, CMP_GREATER_THAN, TRUE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } ) +#define SCORE_LT_VAL(battler, ...) Score(__LINE__, battler, CMP_LESS_THAN, TRUE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } ) + #define FORCED_MOVE(battler) ForcedMove(__LINE__, battler) #define SWITCH(battler, partyIndex) Switch(__LINE__, battler, partyIndex) #define SKIP_TURN(battler) SkipTurn(__LINE__, battler) @@ -845,6 +933,8 @@ struct MoveContext // TODO: u8 zMove:1; u16 allowed:1; u16 explicitAllowed:1; + u16 notExpected:1; // Has effect only with EXPECT_MOVE + u16 explicitNotExpected:1; struct BattlePokemon *target; bool8 explicitTarget; struct TurnRNG rng; @@ -864,6 +954,11 @@ struct ItemContext void OpenTurn(u32 sourceLine); void CloseTurn(u32 sourceLine); void Move(u32 sourceLine, struct BattlePokemon *, struct MoveContext); +void ExpectMove(u32 sourceLine, struct BattlePokemon *, struct MoveContext); +void ExpectMoves(u32 sourceLine, struct BattlePokemon *battler, bool32 notExpected, struct FourMoves moves); +void ExpectSendOut(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex); +void ExpectSwitch(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex); +void Score(u32 sourceLine, struct BattlePokemon *battler, u32 cmp, bool32 toValue, struct TestAIScoreStruct cmpCtx); void ForcedMove(u32 sourceLine, struct BattlePokemon *); void Switch(u32 sourceLine, struct BattlePokemon *, u32 partyIndex); void SkipTurn(u32 sourceLine, struct BattlePokemon *); diff --git a/include/test_runner.h b/include/test_runner.h index 199b2e6cb137..78c41ac31ed6 100644 --- a/include/test_runner.h +++ b/include/test_runner.h @@ -14,11 +14,18 @@ void TestRunner_Battle_RecordExp(u32 battlerId, u32 oldExp, u32 newExp); void TestRunner_Battle_RecordMessage(const u8 *message); void TestRunner_Battle_RecordStatus1(u32 battlerId, u32 status1); void TestRunner_Battle_AfterLastTurn(void); +void TestRunner_Battle_CheckChosenMove(u32 battlerId, u32 moveId, u32 target); +void TestRunner_Battle_CheckSwitch(u32 battlerId, u32 partyIndex); +void TestRunner_Battle_CheckAiMoveScores(u32 battlerId); +void TestRunner_Battle_AISetScore(const char *file, u32 line, u32 battlerId, u32 moveIndex, s32 score); +void TestRunner_Battle_AIAdjustScore(const char *file, u32 line, u32 battlerId, u32 moveIndex, s32 score); void TestRunner_Battle_CheckBattleRecordActionType(u32 battlerId, u32 recordIndex, u32 actionType); u32 TestRunner_Battle_GetForcedAbility(u32 side, u32 partyIndex); +s32 MgbaPrintf_(const char *fmt, ...); + #else #define TestRunner_Battle_RecordAbilityPopUp(...) (void)0 @@ -28,11 +35,18 @@ u32 TestRunner_Battle_GetForcedAbility(u32 side, u32 partyIndex); #define TestRunner_Battle_RecordMessage(...) (void)0 #define TestRunner_Battle_RecordStatus1(...) (void)0 #define TestRunner_Battle_AfterLastTurn(...) (void)0 +#define TestRunner_Battle_CheckChosenMove(...) (void)0 +#define TestRunner_Battle_CheckSwitch(...) (void)0 +#define TestRunner_Battle_CheckAiMoveScores(...) (void)0 +#define TestRunner_Battle_AISetScore(...) (void)0 +#define TestRunner_Battle_AIAdjustScore(...) (void)0 #define TestRunner_Battle_CheckBattleRecordActionType(...) (void)0 #define TestRunner_Battle_GetForcedAbility(...) (u32)0 +#define MgbaPrintf_(...) (u32)0 + #endif #endif diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index b397b05e6923..81ec10f8bf94 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -30,7 +30,7 @@ static u32 ChooseMoveOrAction_Singles(u32 battlerAi); static u32 ChooseMoveOrAction_Doubles(u32 battlerAi); -static inline void BattleAI_DoAIProcessing(struct AI_ThinkingStruct *aiThink, u32 battler); +static inline void BattleAI_DoAIProcessing(struct AI_ThinkingStruct *aiThink, u32 battlerAi, u32 battlerDef); static bool32 IsPinchBerryItemEffect(u32 holdEffect); // ewram @@ -186,9 +186,9 @@ void BattleAI_SetupAIData(u8 defaultScoreMoves, u32 battler) for (i = 0; i < MAX_MON_MOVES; i++) { if (defaultScoreMoves & 1) - AI_THINKING_STRUCT->score[i] = 100; + SET_SCORE(battler, i, AI_SCORE_DEFAULT); else - AI_THINKING_STRUCT->score[i] = 0; + SET_SCORE(battler, i, 0); defaultScoreMoves >>= 1; } @@ -199,7 +199,7 @@ void BattleAI_SetupAIData(u8 defaultScoreMoves, u32 battler) for (i = 0; i < MAX_MON_MOVES; i++) { if (gBitTable[i] & moveLimitations) - AI_THINKING_STRUCT->score[i] = 0; + SET_SCORE(battler, i, 0); } //sBattler_AI = battler; @@ -219,6 +219,9 @@ u32 BattleAI_ChooseMoveOrAction(void) // Clear protect structures, some flags may be set during AI calcs // e.g. pranksterElevated from GetMovePriority memset(&gProtectStructs, 0, MAX_BATTLERS_COUNT * sizeof(struct ProtectStruct)); + #if TESTING + TestRunner_Battle_CheckAiMoveScores(sBattler_AI); + #endif // TESTING return ret; } @@ -497,7 +500,7 @@ static u32 ChooseMoveOrAction_Singles(u32 battlerAi) { if (flags & 1) { - BattleAI_DoAIProcessing(AI_THINKING_STRUCT, battlerAi); + BattleAI_DoAIProcessing(AI_THINKING_STRUCT, battlerAi, gBattlerTarget); } flags >>= 1; AI_THINKING_STRUCT->aiLogicId++; @@ -505,6 +508,7 @@ static u32 ChooseMoveOrAction_Singles(u32 battlerAi) for (i = 0; i < MAX_MON_MOVES; i++) { gBattleStruct->aiFinalScore[battlerAi][gBattlerTarget][i] = AI_THINKING_STRUCT->score[i]; + } // Check special AI actions. @@ -571,7 +575,7 @@ static u32 ChooseMoveOrAction_Doubles(u32 battlerAi) gBattlerTarget = i; - AI_DATA->partnerMove = GetAllyChosenMove(i); + AI_DATA->partnerMove = GetAllyChosenMove(battlerAi); AI_THINKING_STRUCT->aiLogicId = 0; AI_THINKING_STRUCT->movesetIndex = 0; flags = AI_THINKING_STRUCT->aiFlags; @@ -579,7 +583,7 @@ static u32 ChooseMoveOrAction_Doubles(u32 battlerAi) { if (flags & 1) { - BattleAI_DoAIProcessing(AI_THINKING_STRUCT, battlerAi); + BattleAI_DoAIProcessing(AI_THINKING_STRUCT, battlerAi, gBattlerTarget); } flags >>= 1; AI_THINKING_STRUCT->aiLogicId++; @@ -623,7 +627,7 @@ static u32 ChooseMoveOrAction_Doubles(u32 battlerAi) bestMovePointsForTarget[i] = mostViableMovesScores[0]; // Don't use a move against ally if it has less than 100 points. - if (i == BATTLE_PARTNER(battlerAi) && bestMovePointsForTarget[i] < 100) + if (i == BATTLE_PARTNER(battlerAi) && bestMovePointsForTarget[i] < AI_SCORE_DEFAULT) { bestMovePointsForTarget[i] = -1; } @@ -663,25 +667,37 @@ static u32 ChooseMoveOrAction_Doubles(u32 battlerAi) return actionOrMoveIndex[gBattlerTarget]; } -static inline void BattleAI_DoAIProcessing(struct AI_ThinkingStruct *aiThink, u32 battler) +static inline bool32 ShouldConsiderMoveForBattler(u32 battlerAi, u32 battlerDef, u32 move) +{ + if (battlerAi == BATTLE_PARTNER(battlerDef)) + { + if (gBattleMoves[move].target == MOVE_TARGET_BOTH || gBattleMoves[move].target == MOVE_TARGET_OPPONENTS_FIELD) + return FALSE; + } + return TRUE; +} + +static inline void BattleAI_DoAIProcessing(struct AI_ThinkingStruct *aiThink, u32 battlerAi, u32 battlerDef) { do { - if (gBattleMons[battler].pp[aiThink->movesetIndex] == 0) + if (gBattleMons[battlerAi].pp[aiThink->movesetIndex] == 0) aiThink->moveConsidered = MOVE_NONE; else - aiThink->moveConsidered = gBattleMons[battler].moves[aiThink->movesetIndex]; + aiThink->moveConsidered = gBattleMons[battlerAi].moves[aiThink->movesetIndex]; + // There is no point in calculating scores for all 3 battlers(2 opponents + 1 ally) with certain moves. if (aiThink->moveConsidered != MOVE_NONE - && aiThink->score[aiThink->movesetIndex] > 0) + && aiThink->score[aiThink->movesetIndex] > 0 + && ShouldConsiderMoveForBattler(battlerAi, battlerDef, aiThink->moveConsidered)) { if (aiThink->aiLogicId < ARRAY_COUNT(sBattleAiFuncTable) && sBattleAiFuncTable[aiThink->aiLogicId] != NULL) { // Call AI function aiThink->score[aiThink->movesetIndex] = - sBattleAiFuncTable[aiThink->aiLogicId](battler, - gBattlerTarget, + sBattleAiFuncTable[aiThink->aiLogicId](battlerAi, + battlerDef, aiThink->moveConsidered, aiThink->score[aiThink->movesetIndex]); } @@ -705,7 +721,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) u32 moveEffect = gBattleMoves[move].effect; s32 moveType; u32 moveTarget = AI_GetBattlerMoveTargetType(battlerAtk, move); - u32 accuracy = AI_GetMoveAccuracy(battlerAtk, battlerDef, move); + u32 accuracy = AI_DATA->moveAccuracy[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex]; struct AiLogicData *aiData = AI_DATA; u32 effectiveness = aiData->effectiveness[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex]; bool32 isDoubleBattle = IsValidDoubleBattle(battlerAtk); @@ -769,11 +785,11 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_WILL_O_WISP: case EFFECT_TOXIC: case EFFECT_LEECH_SEED: - score -= 5; + ADJUST_SCORE(-5); break; case EFFECT_CURSE: if (IS_BATTLER_OF_TYPE(battlerAtk, TYPE_GHOST)) // Don't use Curse if you're a ghost type vs a Magic Guard user, they'll take no damage. - score -= 5; + ADJUST_SCORE(-5); break; } break; @@ -992,40 +1008,40 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) break; // check move damage case EFFECT_SLEEP: if (!AI_CanPutToSleep(battlerAtk, battlerDef, aiData->abilities[battlerDef], move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_EXPLOSION: if (!(AI_THINKING_STRUCT->aiFlags & AI_FLAG_WILL_SUICIDE)) - score -= 2; + ADJUST_SCORE(-2); if (effectiveness == AI_EFFECTIVENESS_x0) { - score -= 10; + ADJUST_SCORE(-10); } else if (IsAbilityOnField(ABILITY_DAMP) && !DoesBattlerIgnoreAbilityChecks(aiData->abilities[battlerAtk], move)) { - score -= 10; + ADJUST_SCORE(-10); } else if (CountUsablePartyMons(battlerAtk) == 0) { if (CountUsablePartyMons(battlerDef) != 0) - score -= 10; + ADJUST_SCORE(-10); else - score--; + ADJUST_SCORE(-1); } break; case EFFECT_DREAM_EATER: if (!AI_IsBattlerAsleepOrComatose(battlerDef)) - score -= 8; + ADJUST_SCORE(-8); else if (effectiveness == AI_EFFECTIVENESS_x0) - score -= 10; + ADJUST_SCORE(-10); break; // stat raising effects case EFFECT_ATTACK_UP: case EFFECT_ATTACK_UP_2: case EFFECT_ATTACK_UP_USER_ALLY: if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_ATK) || !HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_STUFF_CHEEKS: if (ItemId_GetPocket(gBattleMons[battlerAtk].item) != POCKET_BERRIES) @@ -1036,131 +1052,131 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_DEFENSE_UP_3: case EFFECT_DEFENSE_CURL: if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_DEF)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SPECIAL_ATTACK_UP: case EFFECT_SPECIAL_ATTACK_UP_2: case EFFECT_SPECIAL_ATTACK_UP_3: if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPATK) || !HasMoveWithSplit(battlerAtk, SPLIT_SPECIAL)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SPECIAL_DEFENSE_UP: case EFFECT_SPECIAL_DEFENSE_UP_2: if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPDEF)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_ACCURACY_UP: case EFFECT_ACCURACY_UP_2: if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_ACC)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_EVASION_UP: case EFFECT_EVASION_UP_2: case EFFECT_MINIMIZE: if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_EVASION)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_COSMIC_POWER: if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_DEF)) - score -= 10; + ADJUST_SCORE(-10); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPDEF)) - score -= 8; + ADJUST_SCORE(-8); break; case EFFECT_BULK_UP: if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_ATK) || !HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL)) - score -= 10; + ADJUST_SCORE(-10); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_DEF)) - score -= 8; + ADJUST_SCORE(-8); break; case EFFECT_CALM_MIND: if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPATK)) - score -= 10; + ADJUST_SCORE(-10); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPDEF)) - score -= 8; + ADJUST_SCORE(-8); break; case EFFECT_DRAGON_DANCE: if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_ATK) || !HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL)) - score -= 10; + ADJUST_SCORE(-10); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPEED)) - score -= 8; + ADJUST_SCORE(-8); break; case EFFECT_COIL: if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_ACC)) - score -= 10; + ADJUST_SCORE(-10); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_ATK) || !HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL)) - score -= 8; + ADJUST_SCORE(-8); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_DEF)) - score -= 6; + ADJUST_SCORE(-6); break; case EFFECT_ATTACK_ACCURACY_UP: //hone claws if (aiData->abilities[battlerAtk] != ABILITY_CONTRARY) { if (gBattleMons[battlerAtk].statStages[STAT_ATK] >= MAX_STAT_STAGE && (gBattleMons[battlerAtk].statStages[STAT_ACC] >= MAX_STAT_STAGE || !HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL))) - score -= 10; + ADJUST_SCORE(-10); break; } else { - score -= 10; + ADJUST_SCORE(-10); } break; case EFFECT_CHARGE: if (gStatuses3[battlerAtk] & STATUS3_CHARGED_UP) - score -= 20; + ADJUST_SCORE(-20); else if (!HasMoveWithType(battlerAtk, TYPE_ELECTRIC)) - score -= 10; + ADJUST_SCORE(-10); #if B_CHARGE_SPDEF_RAISE >= GEN_5 else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPDEF)) - score -= 5; + ADJUST_SCORE(-5); #endif break; case EFFECT_QUIVER_DANCE: case EFFECT_GEOMANCY: if (gBattleMons[battlerAtk].statStages[STAT_SPATK] >= MAX_STAT_STAGE || !HasMoveWithSplit(battlerAtk, SPLIT_SPECIAL)) - score -= 10; + ADJUST_SCORE(-10); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPEED)) - score -= 8; + ADJUST_SCORE(-8); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPDEF)) - score -= 6; + ADJUST_SCORE(-6); break; case EFFECT_VICTORY_DANCE: if (gBattleMons[battlerAtk].statStages[STAT_ATK] >= MAX_STAT_STAGE || !HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL)) - score -= 10; + ADJUST_SCORE(-10); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPEED)) - score -= 8; + ADJUST_SCORE(-8); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_DEF)) - score -= 6; + ADJUST_SCORE(-6); break; case EFFECT_SHIFT_GEAR: if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_ATK) || !HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL)) - score -= 10; + ADJUST_SCORE(-10); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPEED)) - score -= 8; + ADJUST_SCORE(-8); break; case EFFECT_SHELL_SMASH: if (aiData->abilities[battlerAtk] == ABILITY_CONTRARY) { if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_DEF)) - score -= 10; + ADJUST_SCORE(-10); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPDEF)) - score -= 8; + ADJUST_SCORE(-8); } else { if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_ATK) || !HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL)) - score -= 10; + ADJUST_SCORE(-10); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPATK) || !HasMoveWithSplit(battlerAtk, SPLIT_SPECIAL)) - score -= 8; + ADJUST_SCORE(-8); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPEED)) - score -= 6; + ADJUST_SCORE(-6); } break; case EFFECT_GROWTH: case EFFECT_ATTACK_SPATK_UP: // work up if ((!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_ATK) && !BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPATK)) || (!HasDamagingMove(battlerAtk))) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_ROTOTILLER: if (isDoubleBattle) @@ -1174,14 +1190,14 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) && (BattlerStatCanRise(BATTLE_PARTNER(battlerAtk), aiData->abilities[BATTLE_PARTNER(battlerAtk)], STAT_ATK) || BattlerStatCanRise(BATTLE_PARTNER(battlerAtk), aiData->abilities[BATTLE_PARTNER(battlerAtk)], STAT_SPATK)))) { - score -= 10; + ADJUST_SCORE(-10); } } else if (!(IS_BATTLER_OF_TYPE(battlerAtk, TYPE_GRASS) && AI_IsBattlerGrounded(battlerAtk) && (BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_ATK) || BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPATK)))) { - score -= 10; + ADJUST_SCORE(-10); } break; case EFFECT_GEAR_UP: @@ -1189,14 +1205,14 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { // same as growth, work up if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_ATK) || !HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL)) - score -= 10; + ADJUST_SCORE(-10); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPATK) || !HasMoveWithSplit(battlerAtk, SPLIT_SPECIAL)) - score -= 8; + ADJUST_SCORE(-8); break; } else if (!isDoubleBattle) { - score -= 10; // no partner and our stats wont rise, so don't use + ADJUST_SCORE(-10); // no partner and our stats wont rise, so don't use } if (isDoubleBattle) @@ -1205,29 +1221,29 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { if ((!BattlerStatCanRise(BATTLE_PARTNER(battlerAtk), aiData->abilities[BATTLE_PARTNER(battlerAtk)], STAT_ATK) || !HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL)) && (!BattlerStatCanRise(BATTLE_PARTNER(battlerAtk), aiData->abilities[BATTLE_PARTNER(battlerAtk)], STAT_SPATK) || !HasMoveWithSplit(battlerAtk, SPLIT_SPECIAL))) - score -= 10; + ADJUST_SCORE(-10); } else if (aiData->abilities[battlerAtk] != ABILITY_PLUS && aiData->abilities[battlerAtk] != ABILITY_MINUS) { - score -= 10; // nor our or our partner's ability is plus/minus + ADJUST_SCORE(-10); // nor our or our partner's ability is plus/minus } } break; case EFFECT_ACUPRESSURE: if (DoesSubstituteBlockMove(battlerAtk, battlerDef, move) || AreBattlersStatsMaxed(battlerDef)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_MAGNETIC_FLUX: if (aiData->abilities[battlerAtk] == ABILITY_PLUS || aiData->abilities[battlerAtk] == ABILITY_MINUS) { if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_DEF)) - score -= 10; + ADJUST_SCORE(-10); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPDEF)) - score -= 8; + ADJUST_SCORE(-8); } else if (!isDoubleBattle) { - score -= 10; // our stats wont rise from this move + ADJUST_SCORE(-10); // our stats wont rise from this move } if (isDoubleBattle) @@ -1235,13 +1251,13 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (aiData->abilities[BATTLE_PARTNER(battlerAtk)] == ABILITY_PLUS || aiData->abilities[BATTLE_PARTNER(battlerAtk)] == ABILITY_MINUS) { if (!BattlerStatCanRise(BATTLE_PARTNER(battlerAtk), aiData->abilities[BATTLE_PARTNER(battlerAtk)], STAT_DEF)) - score -= 10; + ADJUST_SCORE(-10); else if (!BattlerStatCanRise(BATTLE_PARTNER(battlerAtk), aiData->abilities[BATTLE_PARTNER(battlerAtk)], STAT_SPDEF)) - score -= 8; + ADJUST_SCORE(-8); } else if (aiData->abilities[battlerAtk] != ABILITY_PLUS && aiData->abilities[battlerAtk] != ABILITY_MINUS) { - score -= 10; // nor our or our partner's ability is plus/minus + ADJUST_SCORE(-10); // nor our or our partner's ability is plus/minus } } break; @@ -1249,92 +1265,92 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_ATTACK_DOWN: case EFFECT_ATTACK_DOWN_2: if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_ATK)) //|| !HasMoveWithSplit(battlerDef, SPLIT_PHYSICAL)) - score -= 10; + ADJUST_SCORE(-10); else if (aiData->abilities[battlerDef] == ABILITY_HYPER_CUTTER) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_DEFENSE_DOWN: case EFFECT_DEFENSE_DOWN_2: if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_DEF)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SPEED_DOWN: case EFFECT_SPEED_DOWN_2: if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_SPEED)) - score -= 10; + ADJUST_SCORE(-10); else if (aiData->abilities[battlerDef] == ABILITY_SPEED_BOOST) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SPECIAL_ATTACK_DOWN: case EFFECT_SPECIAL_ATTACK_DOWN_2: if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_SPATK)) //|| !HasMoveWithSplit(battlerDef, SPLIT_SPECIAL)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SPECIAL_DEFENSE_DOWN: case EFFECT_SPECIAL_DEFENSE_DOWN_2: if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_SPDEF)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_ACCURACY_DOWN: case EFFECT_ACCURACY_DOWN_2: if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_ACC)) - score -= 10; + ADJUST_SCORE(-10); else if (aiData->abilities[battlerDef] == ABILITY_KEEN_EYE) - score -= 8; + ADJUST_SCORE(-8); break; case EFFECT_EVASION_DOWN: case EFFECT_EVASION_DOWN_2: if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_EVASION)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_TICKLE: if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_ATK)) - score -= 10; + ADJUST_SCORE(-10); else if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_DEF)) - score -= 8; + ADJUST_SCORE(-8); break; case EFFECT_VENOM_DRENCH: if (!(gBattleMons[battlerDef].status1 & STATUS1_PSN_ANY)) { - score -= 10; + ADJUST_SCORE(-10); } else { if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_SPEED)) - score -= 10; + ADJUST_SCORE(-10); else if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_SPATK)) - score -= 8; + ADJUST_SCORE(-8); else if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_ATK)) - score -= 6; + ADJUST_SCORE(-6); } break; case EFFECT_NOBLE_ROAR: if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_SPATK)) - score -= 10; + ADJUST_SCORE(-10); else if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_ATK)) - score -= 8; + ADJUST_SCORE(-8); break; case EFFECT_CAPTIVATE: if (!AreBattlersOfOppositeGender(battlerAtk, battlerDef)) - score -= 10; + ADJUST_SCORE(-10); break; // other case EFFECT_HAZE: if (PartnerHasSameMoveEffectWithoutTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) { - score -= 10; // partner already using haze + ADJUST_SCORE(-10); // partner already using haze } else { for (i = STAT_ATK; i < NUM_BATTLE_STATS; i++) { if (gBattleMons[battlerAtk].statStages[i] > DEFAULT_STAT_STAGE || gBattleMons[BATTLE_PARTNER(battlerAtk)].statStages[i] > DEFAULT_STAT_STAGE) - score -= 10; // Don't want to reset our boosted stats + ADJUST_SCORE(-10); // Don't want to reset our boosted stats } for (i = STAT_ATK; i < NUM_BATTLE_STATS; i++) { if (gBattleMons[battlerDef].statStages[i] < DEFAULT_STAT_STAGE || gBattleMons[BATTLE_PARTNER(battlerDef)].statStages[i] < DEFAULT_STAT_STAGE) - score -= 10; //Don't want to reset enemy lowered stats + ADJUST_SCORE(-10); //Don't want to reset enemy lowered stats } } break; @@ -1357,47 +1373,47 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_LOW_KICK: // AI_CBM_HighRiskForDamage if (aiData->abilities[battlerDef] == ABILITY_WONDER_GUARD && effectiveness < AI_EFFECTIVENESS_x2) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_COUNTER: case EFFECT_MIRROR_COAT: if (IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) || gBattleMons[battlerDef].status2 & (STATUS2_INFATUATION | STATUS2_CONFUSION)) - score--; + ADJUST_SCORE(-1); if (predictedMove == MOVE_NONE || GetBattleMoveSplit(predictedMove) == SPLIT_STATUS || DoesSubstituteBlockMove(battlerAtk, BATTLE_PARTNER(battlerDef), predictedMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_ROAR: if (CountUsablePartyMons(battlerDef) == 0) - score -= 10; + ADJUST_SCORE(-10); else if (aiData->abilities[battlerDef] == ABILITY_SUCTION_CUPS) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_TOXIC_THREAD: if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_SPEED)) - score--; // may still want to just poison + ADJUST_SCORE(-1); // may still want to just poison //fallthrough case EFFECT_POISON: case EFFECT_TOXIC: if (!AI_CanPoison(battlerAtk, battlerDef, aiData->abilities[battlerDef], move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_LIGHT_SCREEN: if (gSideStatuses[GetBattlerSide(battlerAtk)] & SIDE_STATUS_LIGHTSCREEN || PartnerHasSameMoveEffectWithoutTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_REFLECT: if (gSideStatuses[GetBattlerSide(battlerAtk)] & SIDE_STATUS_REFLECT || PartnerHasSameMoveEffectWithoutTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_AURORA_VEIL: if (gSideStatuses[GetBattlerSide(battlerAtk)] & SIDE_STATUS_AURORA_VEIL || PartnerHasSameMoveEffectWithoutTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove) || !(weather & (B_WEATHER_HAIL | B_WEATHER_SNOW))) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_OHKO: #if B_SHEER_COLD_IMMUNITY >= GEN_7 @@ -1405,44 +1421,44 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) return 0; #endif if (!ShouldTryOHKO(battlerAtk, battlerDef, aiData->abilities[battlerAtk], aiData->abilities[battlerDef], move)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_MIST: if (gSideStatuses[GetBattlerSide(battlerAtk)] & SIDE_STATUS_MIST || PartnerHasSameMoveEffectWithoutTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_FOCUS_ENERGY: if (gBattleMons[battlerAtk].status2 & STATUS2_FOCUS_ENERGY) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_CONFUSE: case EFFECT_SWAGGER: case EFFECT_FLATTER: if (!AI_CanConfuse(battlerAtk, battlerDef, aiData->abilities[battlerDef], BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_PARALYZE: if (!AI_CanParalyze(battlerAtk, battlerDef, aiData->abilities[battlerDef], move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SUBSTITUTE: if (gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE || aiData->abilities[battlerDef] == ABILITY_INFILTRATOR) - score -= 8; + ADJUST_SCORE(-8); else if (aiData->hpPercents[battlerAtk] <= 25) - score -= 10; + ADJUST_SCORE(-10); #if B_SOUND_SUBSTITUTE >= GEN_6 else if (HasSoundMove(battlerDef)) - score -= 8; + ADJUST_SCORE(-8); #endif break; case EFFECT_LEECH_SEED: if (gStatuses3[battlerDef] & STATUS3_LEECHSEED || IS_BATTLER_OF_TYPE(battlerDef, TYPE_GRASS) || DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); else if (aiData->abilities[battlerDef] == ABILITY_LIQUID_OOZE) - score -= 3; + ADJUST_SCORE(-3); break; case EFFECT_DISABLE: if (gDisableStructs[battlerDef].disableTimer == 0 @@ -1454,16 +1470,16 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER) // Attacker should go first { if (gLastMoves[battlerDef] == MOVE_NONE || gLastMoves[battlerDef] == 0xFFFF) - score -= 10; // no anticipated move to disable + ADJUST_SCORE(-10); // no anticipated move to disable } else if (predictedMove == MOVE_NONE) { - score -= 10; + ADJUST_SCORE(-10); } } else { - score -= 10; + ADJUST_SCORE(-10); } break; case EFFECT_ENCORE: @@ -1476,83 +1492,83 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER) // Attacker should go first { if (gLastMoves[battlerDef] == MOVE_NONE || gLastMoves[battlerDef] == 0xFFFF) - score -= 10; // no anticipated move to encore + ADJUST_SCORE(-10); // no anticipated move to encore } else if (predictedMove == MOVE_NONE) { - score -= 10; + ADJUST_SCORE(-10); } } else { - score -= 10; + ADJUST_SCORE(-10); } break; case EFFECT_SNORE: case EFFECT_SLEEP_TALK: if (IsWakeupTurn(battlerAtk) || !AI_IsBattlerAsleepOrComatose(battlerAtk)) - score -= 10; // if mon will wake up, is not asleep, or is not comatose + ADJUST_SCORE(-10); // if mon will wake up, is not asleep, or is not comatose break; case EFFECT_MEAN_LOOK: if (IsBattlerTrapped(battlerDef, TRUE) || DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_NIGHTMARE: if (gBattleMons[battlerDef].status2 & STATUS2_NIGHTMARE) - score -= 10; + ADJUST_SCORE(-10); else if (!AI_IsBattlerAsleepOrComatose(battlerDef)) - score -= 8; + ADJUST_SCORE(-8); else if (DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_CURSE: if (IS_BATTLER_OF_TYPE(battlerAtk, TYPE_GHOST)) { if (gBattleMons[battlerDef].status2 & STATUS2_CURSED || DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); else if (aiData->hpPercents[battlerAtk] <= 50) - score -= 6; + ADJUST_SCORE(-6); } else // regular curse { if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_ATK) || !HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL)) - score -= 10; + ADJUST_SCORE(-10); else if (!BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_DEF)) - score -= 8; + ADJUST_SCORE(-8); } break; case EFFECT_SPIKES: if (gSideTimers[GetBattlerSide(battlerDef)].spikesAmount >= 3) - score -= 10; + ADJUST_SCORE(-10); else if (PartnerMoveIsSameNoTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove) && gSideTimers[GetBattlerSide(battlerDef)].spikesAmount == 2) - score -= 10; // only one mon needs to set up the last layer of Spikes + ADJUST_SCORE(-10); // only one mon needs to set up the last layer of Spikes break; case EFFECT_STEALTH_ROCK: if (gSideTimers[GetBattlerSide(battlerDef)].stealthRockAmount > 0 || PartnerMoveIsSameNoTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) //Only one mon needs to set up Stealth Rocks - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_TOXIC_SPIKES: if (gSideTimers[GetBattlerSide(battlerDef)].toxicSpikesAmount >= 2) - score -= 10; + ADJUST_SCORE(-10); else if (PartnerMoveIsSameNoTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove) && gSideTimers[GetBattlerSide(battlerDef)].toxicSpikesAmount == 1) - score -= 10; // only one mon needs to set up the last layer of Toxic Spikes + ADJUST_SCORE(-10); // only one mon needs to set up the last layer of Toxic Spikes break; case EFFECT_STICKY_WEB: if (gSideTimers[GetBattlerSide(battlerDef)].stickyWebAmount) - score -= 10; + ADJUST_SCORE(-10); else if (PartnerMoveIsSameNoTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove) && gSideTimers[GetBattlerSide(battlerDef)].stickyWebAmount) - score -= 10; // only one mon needs to set up Sticky Web + ADJUST_SCORE(-10); // only one mon needs to set up Sticky Web break; case EFFECT_FORESIGHT: if (gBattleMons[battlerDef].status2 & STATUS2_FORESIGHT) - score -= 10; + ADJUST_SCORE(-10); else if (gBattleMons[battlerDef].statStages[STAT_EVASION] <= 4 || !(IS_BATTLER_OF_TYPE(battlerDef, TYPE_GHOST)) || DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 9; + ADJUST_SCORE(-9); break; case EFFECT_PERISH_SONG: if (isDoubleBattle) @@ -1562,85 +1578,85 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) && aiData->abilities[BATTLE_PARTNER(battlerAtk)] != ABILITY_SOUNDPROOF && CountUsablePartyMons(FOE(battlerAtk)) >= 1) { - score -= 10; //Don't wipe your team if you're going to lose + ADJUST_SCORE(-10); //Don't wipe your team if you're going to lose } else if ((!IsBattlerAlive(FOE(battlerAtk)) || aiData->abilities[FOE(battlerAtk)] == ABILITY_SOUNDPROOF || gStatuses3[FOE(battlerAtk)] & STATUS3_PERISH_SONG) && (!IsBattlerAlive(BATTLE_PARTNER(FOE(battlerAtk))) || aiData->abilities[BATTLE_PARTNER(FOE(battlerAtk))] == ABILITY_SOUNDPROOF || gStatuses3[BATTLE_PARTNER(FOE(battlerAtk))] & STATUS3_PERISH_SONG)) { - score -= 10; //Both enemies are perish songed + ADJUST_SCORE(-10); //Both enemies are perish songed } else if (DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) { - score -= 10; + ADJUST_SCORE(-10); } } else { if (CountUsablePartyMons(battlerAtk) == 0 && aiData->abilities[battlerAtk] != ABILITY_SOUNDPROOF && CountUsablePartyMons(battlerDef) >= 1) - score -= 10; + ADJUST_SCORE(-10); if (gStatuses3[FOE(battlerAtk)] & STATUS3_PERISH_SONG || aiData->abilities[FOE(battlerAtk)] == ABILITY_SOUNDPROOF) - score -= 10; + ADJUST_SCORE(-10); } break; case EFFECT_SANDSTORM: if (weather & (B_WEATHER_SANDSTORM | B_WEATHER_PRIMAL_ANY) - || PartnerMoveEffectIsWeather(BATTLE_PARTNER(battlerAtk), aiData->partnerMove)) - score -= 8; + || IsMoveEffectWeather(aiData->partnerMove)) + ADJUST_SCORE(-8); break; case EFFECT_SUNNY_DAY: if (weather & (B_WEATHER_SUN | B_WEATHER_PRIMAL_ANY) - || PartnerMoveEffectIsWeather(BATTLE_PARTNER(battlerAtk), aiData->partnerMove)) - score -= 8; + || IsMoveEffectWeather(aiData->partnerMove)) + ADJUST_SCORE(-8); break; case EFFECT_RAIN_DANCE: if (weather & (B_WEATHER_RAIN | B_WEATHER_PRIMAL_ANY) - || PartnerMoveEffectIsWeather(BATTLE_PARTNER(battlerAtk), aiData->partnerMove)) - score -= 8; + || IsMoveEffectWeather(aiData->partnerMove)) + ADJUST_SCORE(-8); break; case EFFECT_HAIL: if (weather & (B_WEATHER_HAIL | B_WEATHER_PRIMAL_ANY) - || PartnerMoveEffectIsWeather(BATTLE_PARTNER(battlerAtk), aiData->partnerMove)) - score -= 8; + || IsMoveEffectWeather(aiData->partnerMove)) + ADJUST_SCORE(-8); else if (weather & B_WEATHER_SNOW) - score -= 2; // mainly to prevent looping between hail and snow + ADJUST_SCORE(-2); // mainly to prevent looping between hail and snow break; case EFFECT_SNOWSCAPE: if (weather & (B_WEATHER_SNOW | B_WEATHER_PRIMAL_ANY) - || PartnerMoveEffectIsWeather(BATTLE_PARTNER(battlerAtk), aiData->partnerMove)) - score -= 8; + || IsMoveEffectWeather(aiData->partnerMove)) + ADJUST_SCORE(-8); else if (weather & B_WEATHER_HAIL) - score -= 2; // mainly to prevent looping between hail and snow + ADJUST_SCORE(-2); // mainly to prevent looping between hail and snow break; case EFFECT_ATTRACT: if (!AI_CanBeInfatuated(battlerAtk, battlerDef, aiData->abilities[battlerDef])) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SAFEGUARD: if (gSideStatuses[GetBattlerSide(battlerAtk)] & SIDE_STATUS_SAFEGUARD || PartnerHasSameMoveEffectWithoutTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_MAGNITUDE: if (aiData->abilities[battlerDef] == ABILITY_LEVITATE) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_PARTING_SHOT: if (CountUsablePartyMons(battlerAtk) == 0) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_BATON_PASS: if (CountUsablePartyMons(battlerAtk) == 0) - score -= 10; + ADJUST_SCORE(-10); else if (gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE || (gStatuses3[battlerAtk] & (STATUS3_ROOTED | STATUS3_AQUA_RING | STATUS3_MAGNET_RISE | STATUS3_POWER_TRICK)) || AnyStatIsRaised(battlerAtk)) break; else - score -= 6; + ADJUST_SCORE(-6); break; case EFFECT_HIT_ESCAPE: break; @@ -1649,70 +1665,70 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) break; // check damage/accuracy //Spin checks if (!(gSideStatuses[GetBattlerSide(battlerAtk)] & SIDE_STATUS_HAZARDS_ANY)) - score -= 6; + ADJUST_SCORE(-6); break; case EFFECT_BELLY_DRUM: if (aiData->abilities[battlerAtk] == ABILITY_CONTRARY) - score -= 10; + ADJUST_SCORE(-10); else if (aiData->hpPercents[battlerAtk] <= 60) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_FUTURE_SIGHT: if (gSideStatuses[GetBattlerSide(battlerDef)] & SIDE_STATUS_FUTUREATTACK || gSideStatuses[GetBattlerSide(battlerAtk)] & SIDE_STATUS_FUTUREATTACK) - score -= 12; + ADJUST_SCORE(-12); else - score += 5; + ADJUST_SCORE(5); break; case EFFECT_TELEPORT: - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_FAKE_OUT: if (!gDisableStructs[battlerAtk].isFirstTurn) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_STOCKPILE: if (gDisableStructs[battlerAtk].stockpileCounter >= 3) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SPIT_UP: if (gDisableStructs[battlerAtk].stockpileCounter <= 1) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SWALLOW: if (gDisableStructs[battlerAtk].stockpileCounter == 0) { - score -= 10; + ADJUST_SCORE(-10); } else { if (AtMaxHp(battlerAtk)) - score -= 10; + ADJUST_SCORE(-10); else if (aiData->hpPercents[battlerAtk] >= 80) - score -= 5; // do it if nothing better + ADJUST_SCORE(-5); // do it if nothing better } break; case EFFECT_TORMENT: if (gBattleMons[battlerDef].status2 & STATUS2_TORMENT || DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) { - score -= 10; + ADJUST_SCORE(-10); break; } #if B_MENTAL_HERB >= GEN_5 if (aiData->holdEffects[battlerDef] == HOLD_EFFECT_MENTAL_HERB) - score -= 6; + ADJUST_SCORE(-6); #endif break; case EFFECT_WILL_O_WISP: if (!AI_CanBurn(battlerAtk, battlerDef, aiData->abilities[battlerDef], BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_MEMENTO: if (CountUsablePartyMons(battlerAtk) == 0 || DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); else if (gBattleMons[battlerDef].statStages[STAT_ATK] == MIN_STAT_STAGE && gBattleMons[battlerDef].statStages[STAT_SPATK] == MIN_STAT_STAGE) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_FOLLOW_ME: case EFFECT_HELPING_HAND: @@ -1721,70 +1737,70 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) || PartnerHasSameMoveEffectWithoutTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove) || (aiData->partnerMove != MOVE_NONE && IS_MOVE_STATUS(aiData->partnerMove)) || *(gBattleStruct->monToSwitchIntoId + BATTLE_PARTNER(battlerAtk)) != PARTY_SIZE) //Partner is switching out. - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_TRICK: case EFFECT_KNOCK_OFF: if (aiData->abilities[battlerDef] == ABILITY_STICKY_HOLD) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_INGRAIN: if (gStatuses3[battlerAtk] & STATUS3_ROOTED) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_AQUA_RING: if (gStatuses3[battlerAtk] & STATUS3_AQUA_RING) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_RECYCLE: if (GetUsedHeldItem(battlerAtk) == 0 || gBattleMons[battlerAtk].item != 0) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_IMPRISON: if (gStatuses3[battlerAtk] & STATUS3_IMPRISONED_OTHERS) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_REFRESH: if (!(gBattleMons[battlerDef].status1 & (STATUS1_PSN_ANY | STATUS1_BURN | STATUS1_PARALYSIS | STATUS1_FROSTBITE))) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_PSYCHO_SHIFT: if (gBattleMons[battlerAtk].status1 & STATUS1_PSN_ANY && !AI_CanPoison(battlerAtk, battlerDef, aiData->abilities[battlerDef], move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); else if (gBattleMons[battlerAtk].status1 & STATUS1_BURN && !AI_CanBurn(battlerAtk, battlerDef, aiData->abilities[battlerDef], BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); else if (gBattleMons[battlerAtk].status1 & STATUS1_FROSTBITE && !AI_CanGiveFrostbite(battlerAtk, battlerDef, aiData->abilities[battlerDef], BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); else if (gBattleMons[battlerAtk].status1 & STATUS1_PARALYSIS && !AI_CanParalyze(battlerAtk, battlerDef, aiData->abilities[battlerDef], move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); else if (gBattleMons[battlerAtk].status1 & STATUS1_SLEEP && !AI_CanPutToSleep(battlerAtk, battlerDef, aiData->abilities[battlerDef], move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); else - score -= 10; // attacker has no status to transmit + ADJUST_SCORE(-10); // attacker has no status to transmit break; case EFFECT_MUD_SPORT: if (gFieldStatuses & STATUS_FIELD_MUDSPORT || gStatuses4[battlerAtk] & STATUS4_MUD_SPORT || PartnerHasSameMoveEffectWithoutTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_WATER_SPORT: if (gFieldStatuses & STATUS_FIELD_WATERSPORT || gStatuses4[battlerAtk] & STATUS4_WATER_SPORT || PartnerHasSameMoveEffectWithoutTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_ABSORB: if (aiData->abilities[battlerDef] == ABILITY_LIQUID_OOZE) - score -= 6; + ADJUST_SCORE(-6); break; case EFFECT_STRENGTH_SAP: if (aiData->abilities[battlerDef] == ABILITY_CONTRARY) - score -= 10; + ADJUST_SCORE(-10); else if (!ShouldLowerStat(battlerDef, aiData->abilities[battlerDef], STAT_ATK)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_COPYCAT: case EFFECT_MIRROR_MOVE: @@ -1792,77 +1808,77 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_FLOWER_SHIELD: if (!IS_BATTLER_OF_TYPE(battlerAtk, TYPE_GRASS) && !(isDoubleBattle && IS_BATTLER_OF_TYPE(BATTLE_PARTNER(battlerAtk), TYPE_GRASS))) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_AROMATIC_MIST: if (!isDoubleBattle || gBattleMons[BATTLE_PARTNER(battlerAtk)].hp == 0 || !BattlerStatCanRise(BATTLE_PARTNER(battlerAtk), aiData->abilities[BATTLE_PARTNER(battlerAtk)], STAT_SPDEF)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_BIDE: if (!HasDamagingMove(battlerDef) || aiData->hpPercents[battlerAtk] < 30 //Close to death || gBattleMons[battlerDef].status1 & (STATUS1_SLEEP | STATUS1_FREEZE)) //No point in biding if can't take damage - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_HIT_SWITCH_TARGET: if (DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; // don't scare away pokemon twice + ADJUST_SCORE(-10); // don't scare away pokemon twice else if (aiData->hpPercents[battlerDef] < 10 && GetBattlerSecondaryDamage(battlerDef)) - score -= 10; // don't blow away mon that will faint soon + ADJUST_SCORE(-10); // don't blow away mon that will faint soon else if (gStatuses3[battlerDef] & STATUS3_PERISH_SONG) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_CONVERSION: //Check first move type if (IS_BATTLER_OF_TYPE(battlerAtk, gBattleMoves[gBattleMons[battlerAtk].moves[0]].type)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_REST: if (!AI_CanSleep(battlerAtk, aiData->abilities[battlerAtk])) - score -= 10; + ADJUST_SCORE(-10); //fallthrough case EFFECT_RESTORE_HP: case EFFECT_SOFTBOILED: case EFFECT_ROOST: if (AtMaxHp(battlerAtk)) - score -= 10; + ADJUST_SCORE(-10); else if (aiData->hpPercents[battlerAtk] >= 90) - score -= 9; //No point in healing, but should at least do it if nothing better + ADJUST_SCORE(-9); //No point in healing, but should at least do it if nothing better break; case EFFECT_MORNING_SUN: case EFFECT_SYNTHESIS: case EFFECT_MOONLIGHT: if ((AI_GetWeather(aiData) & (B_WEATHER_RAIN | B_WEATHER_SANDSTORM | B_WEATHER_HAIL))) - score -= 3; + ADJUST_SCORE(-3); else if (AtMaxHp(battlerAtk)) - score -= 10; + ADJUST_SCORE(-10); else if (aiData->hpPercents[battlerAtk] >= 90) - score -= 9; //No point in healing, but should at least do it if nothing better + ADJUST_SCORE(-9); //No point in healing, but should at least do it if nothing better break; case EFFECT_PURIFY: if (!(gBattleMons[battlerDef].status1 & STATUS1_ANY)) - score -= 10; + ADJUST_SCORE(-10); else if (battlerDef == BATTLE_PARTNER(battlerAtk)) break; //Always heal your ally else if (AtMaxHp(battlerAtk)) - score -= 10; + ADJUST_SCORE(-10); else if (aiData->hpPercents[battlerAtk] >= 90) - score -= 8; //No point in healing, but should at least do it if nothing better + 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) - score -= 4; + ADJUST_SCORE(-4); break; case EFFECT_RECOIL_IF_MISS: - if (aiData->abilities[battlerAtk] != ABILITY_MAGIC_GUARD && AI_GetMoveAccuracy(battlerAtk, battlerDef, move) < 75) - score -= 6; + if (aiData->abilities[battlerAtk] != ABILITY_MAGIC_GUARD && AI_DATA->moveAccuracy[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex] < 75) + ADJUST_SCORE(-6); break; case EFFECT_RECOIL_25: if (AI_IsDamagedByRecoil(battlerAtk)) { u32 recoilDmg = max(1, aiData->simulatedDmg[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex] / 4); if (!ShouldUseRecoilMove(battlerAtk, battlerDef, recoilDmg, AI_THINKING_STRUCT->movesetIndex)) - score -= 10; + ADJUST_SCORE(-10); break; } break; @@ -1872,7 +1888,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { u32 recoilDmg = max(1, aiData->simulatedDmg[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex] / 3); if (!ShouldUseRecoilMove(battlerAtk, battlerDef, recoilDmg, AI_THINKING_STRUCT->movesetIndex)) - score -= 10; + ADJUST_SCORE(-10); break; } break; @@ -1881,7 +1897,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { u32 recoilDmg = max(1, aiData->simulatedDmg[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex] / 2); if (!ShouldUseRecoilMove(battlerAtk, battlerDef, recoilDmg, AI_THINKING_STRUCT->movesetIndex)) - score -= 10; + ADJUST_SCORE(-10); break; } break; @@ -1895,24 +1911,24 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) || (IsBattlerGrounded(BATTLE_PARTNER(battlerDef)) && AI_IsTerrainAffected(BATTLE_PARTNER(battlerDef), STATUS_FIELD_MISTY_TERRAIN)) || (DoesSubstituteBlockMove(battlerAtk, BATTLE_PARTNER(battlerDef), move)))) { - score -= 10; + ADJUST_SCORE(-10); } break; case EFFECT_TRANSFORM: if (gBattleMons[battlerAtk].status2 & STATUS2_TRANSFORMED || (gBattleMons[battlerDef].status2 & (STATUS2_TRANSFORMED | STATUS2_SUBSTITUTE))) //Leave out Illusion b/c AI is supposed to be fooled - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_TWO_TURNS_ATTACK: if (aiData->holdEffects[battlerAtk] != HOLD_EFFECT_POWER_HERB && CanTargetFaintAi(battlerDef, battlerAtk)) - score -= 6; + ADJUST_SCORE(-6); break; case EFFECT_RECHARGE: if (aiData->abilities[battlerDef] == ABILITY_WONDER_GUARD && effectiveness < AI_EFFECTIVENESS_x2) - score -= 10; + ADJUST_SCORE(-10); else if (aiData->abilities[battlerAtk] != ABILITY_TRUANT && !CanIndexMoveFaintTarget(battlerAtk, battlerDef, AI_THINKING_STRUCT->movesetIndex, 0)) - score -= 2; + ADJUST_SCORE(-2); break; case EFFECT_SPITE: case EFFECT_MIMIC: @@ -1920,12 +1936,12 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { if (gLastMoves[battlerDef] == MOVE_NONE || gLastMoves[battlerDef] == 0xFFFF) - score -= 10; + ADJUST_SCORE(-10); } else if (predictedMove == MOVE_NONE) { // TODO predicted move separate from gLastMoves - score -= 10; + ADJUST_SCORE(-10); } break; case EFFECT_METRONOME: @@ -1933,7 +1949,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_ENDEAVOR: case EFFECT_PAIN_SPLIT: if (gBattleMons[battlerAtk].hp > (gBattleMons[battlerAtk].hp + gBattleMons[battlerDef].hp) / 2) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_CONVERSION_2: @@ -1944,34 +1960,34 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) || aiData->abilities[battlerAtk] == ABILITY_NO_GUARD || aiData->abilities[battlerDef] == ABILITY_NO_GUARD || DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_LASER_FOCUS: if (gStatuses3[battlerAtk] & STATUS3_LASER_FOCUS) - score -= 10; + ADJUST_SCORE(-10); else if (aiData->abilities[battlerDef] == ABILITY_SHELL_ARMOR || aiData->abilities[battlerDef] == ABILITY_BATTLE_ARMOR) - score -= 8; + ADJUST_SCORE(-8); break; case EFFECT_SKETCH: if (gLastMoves[battlerDef] == MOVE_NONE) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_DESTINY_BOND: if (gBattleMons[battlerDef].status2 & STATUS2_DESTINY_BOND) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_FALSE_SWIPE: // TODO break; case EFFECT_HEAL_BELL: if (!AnyPartyMemberStatused(battlerAtk, gBattleMoves[move].soundMove) || PartnerHasSameMoveEffectWithoutTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_HIT_PREVENT_ESCAPE: break; case EFFECT_ENDURE: if (gBattleMons[battlerAtk].hp == 1 || GetBattlerSecondaryDamage(battlerAtk)) //Don't use Endure if you'll die after using it - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_PROTECT: { @@ -1983,14 +1999,14 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case MOVE_CRAFTY_SHIELD: if (!isDoubleBattle) { - score -= 10; + ADJUST_SCORE(-10); decreased = TRUE; } break; case MOVE_MAT_BLOCK: if (!gDisableStructs[battlerAtk].isFirstTurn) { - score -= 10; + ADJUST_SCORE(-10); decreased = TRUE; } break; @@ -2000,7 +2016,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) break; if (IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef])) { - score -= 10; + ADJUST_SCORE(-10); break; } @@ -2012,18 +2028,18 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) && aiData->abilities[battlerDef] != ABILITY_MOXIE && aiData->abilities[battlerDef] != ABILITY_BEAST_BOOST) { - score -= 10; //Don't protect if you're going to faint after protecting + ADJUST_SCORE(-10); //Don't protect if you're going to faint after protecting } else if (gDisableStructs[battlerAtk].protectUses == 1 && Random() % 100 < 50) { if (!isDoubleBattle) - score -= 6; + ADJUST_SCORE(-6); else - score -= 10; //Don't try double protecting in doubles + ADJUST_SCORE(-10); //Don't try double protecting in doubles } else if (gDisableStructs[battlerAtk].protectUses >= 2) { - score -= 10; + ADJUST_SCORE(-10); } } @@ -2040,20 +2056,20 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) break; case EFFECT_MIRACLE_EYE: if (gStatuses3[battlerDef] & STATUS3_MIRACLE_EYED) - score -= 10; + ADJUST_SCORE(-10); if (gBattleMons[battlerDef].statStages[STAT_EVASION] <= 4 || !(IS_BATTLER_OF_TYPE(battlerDef, TYPE_DARK)) || DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 9; + ADJUST_SCORE(-9); break; case EFFECT_BURN_UP: if (!IS_BATTLER_OF_TYPE(battlerAtk, TYPE_FIRE)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_DOUBLE_SHOCK: if (!IS_BATTLER_OF_TYPE(battlerAtk, TYPE_ELECTRIC)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_DEFOG: if (gSideStatuses[GetBattlerSide(battlerDef)] @@ -2063,14 +2079,14 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { if (PartnerHasSameMoveEffectWithoutTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) { - score -= 10; //Only need one hazards removal + ADJUST_SCORE(-10); //Only need one hazards removal break; } } if (gSideStatuses[GetBattlerSide(battlerDef)] & SIDE_STATUS_HAZARDS_ANY) { - score -= 10; //Don't blow away opposing hazards + ADJUST_SCORE(-10); //Don't blow away opposing hazards break; } @@ -2079,7 +2095,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (IsHazardMoveEffect(gBattleMoves[aiData->partnerMove].effect) // partner is going to set up hazards && AI_WhoStrikesFirst(BATTLE_PARTNER(battlerAtk), battlerAtk, aiData->partnerMove) == AI_IS_FASTER) // partner is going to set up before the potential Defog { - score -= 10; + ADJUST_SCORE(-10); break; // Don't use Defog if partner is going to set up hazards } } @@ -2087,7 +2103,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) // evasion check if (gBattleMons[battlerDef].statStages[STAT_EVASION] == MIN_STAT_STAGE || ((aiData->abilities[battlerDef] == ABILITY_CONTRARY) && !IS_TARGETING_PARTNER(battlerAtk, battlerDef))) // don't want to raise target stats unless its your partner - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_PSYCH_UP: // haze stats check @@ -2095,12 +2111,12 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) for (i = STAT_ATK; i < NUM_BATTLE_STATS; i++) { if (gBattleMons[battlerAtk].statStages[i] > DEFAULT_STAT_STAGE || gBattleMons[BATTLE_PARTNER(battlerAtk)].statStages[i] > DEFAULT_STAT_STAGE) - score -= 10; // Don't want to reset our boosted stats + ADJUST_SCORE(-10); // Don't want to reset our boosted stats } for (i = STAT_ATK; i < NUM_BATTLE_STATS; i++) { if (gBattleMons[battlerDef].statStages[i] < DEFAULT_STAT_STAGE || gBattleMons[BATTLE_PARTNER(battlerDef)].statStages[i] < DEFAULT_STAT_STAGE) - score -= 10; //Don't want to copy enemy lowered stats + ADJUST_SCORE(-10); //Don't want to copy enemy lowered stats } } break; @@ -2111,94 +2127,94 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) || ((AI_GetWeather(aiData) & B_WEATHER_SUN) && aiData->holdEffects[battlerAtk] != HOLD_EFFECT_UTILITY_UMBRELLA)) break; if (CanTargetFaintAi(battlerDef, battlerAtk)) //Attacker can be knocked out - score -= 4; + ADJUST_SCORE(-4); break; case EFFECT_SEMI_INVULNERABLE: if (predictedMove != MOVE_NONE && AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_SLOWER && gBattleMoves[predictedMove].effect == EFFECT_SEMI_INVULNERABLE) - score -= 10; // Don't Fly/dig/etc if opponent is going to fly/dig/etc after you + ADJUST_SCORE(-10); // Don't Fly/dig/etc if opponent is going to fly/dig/etc after you if (BattlerWillFaintFromWeather(battlerAtk, aiData->abilities[battlerAtk]) && (move == MOVE_FLY || move == MOVE_BOUNCE)) - score -= 10; // Attacker will faint while in the air + ADJUST_SCORE(-10); // Attacker will faint while in the air break; case EFFECT_HEALING_WISH: //healing wish, lunar dance if (CountUsablePartyMons(battlerAtk) == 0 || DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); else if (IsPartyFullyHealedExceptBattler(battlerAtk)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_FINAL_GAMBIT: if (CountUsablePartyMons(battlerAtk) == 0 || DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_NATURE_POWER: return AI_CheckBadMove(battlerAtk, battlerDef, GetNaturePowerMove(), score); case EFFECT_TAUNT: if (gDisableStructs[battlerDef].tauntTimer > 0 || DoesPartnerHaveSameMoveEffect(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_BESTOW: if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_NONE || !CanBattlerGetOrLoseItem(battlerAtk, gBattleMons[battlerAtk].item)) // AI knows its own item - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_ROLE_PLAY: if (aiData->abilities[battlerAtk] == aiData->abilities[battlerDef] || aiData->abilities[battlerDef] == ABILITY_NONE || IsRolePlayBannedAbilityAtk(aiData->abilities[battlerAtk]) || IsRolePlayBannedAbility(aiData->abilities[battlerDef])) - score -= 10; + ADJUST_SCORE(-10); else if (IsAbilityOfRating(aiData->abilities[battlerAtk], 5)) - score -= 4; + ADJUST_SCORE(-4); break; case EFFECT_WISH: if (gWishFutureKnock.wishCounter[battlerAtk] != 0) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_ASSIST: if (CountUsablePartyMons(battlerAtk) == 0) - score -= 10; // no teammates to assist from + ADJUST_SCORE(-10); // no teammates to assist from break; case EFFECT_MAGIC_COAT: if (!HasMagicCoatAffectedMove(battlerDef)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_BELCH: if (ItemId_GetPocket(GetUsedHeldItem(battlerAtk)) != POCKET_BERRIES) - score -= 10; // attacker has not consumed a berry + ADJUST_SCORE(-10); // attacker has not consumed a berry break; case EFFECT_YAWN: if (gStatuses3[battlerDef] & STATUS3_YAWN) - score -= 10; + ADJUST_SCORE(-10); else if (!AI_CanPutToSleep(battlerAtk, battlerDef, aiData->abilities[battlerDef], move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SKILL_SWAP: if (aiData->abilities[battlerAtk] == ABILITY_NONE || aiData->abilities[battlerDef] == ABILITY_NONE || IsSkillSwapBannedAbility(aiData->abilities[battlerAtk]) || IsSkillSwapBannedAbility(aiData->abilities[battlerDef]) || aiData->holdEffects[battlerDef] == HOLD_EFFECT_ABILITY_SHIELD) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_WORRY_SEED: if (aiData->abilities[battlerDef] == ABILITY_INSOMNIA || IsWorrySeedBannedAbility(aiData->abilities[battlerDef]) || aiData->holdEffects[battlerDef] == HOLD_EFFECT_ABILITY_SHIELD) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_GASTRO_ACID: if (gStatuses3[battlerDef] & STATUS3_GASTRO_ACID || IsGastroAcidBannedAbility(aiData->abilities[battlerDef])) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_ENTRAINMENT: if (aiData->abilities[battlerAtk] == ABILITY_NONE || IsEntrainmentBannedAbilityAttacker(aiData->abilities[battlerAtk]) || IsEntrainmentTargetOrSimpleBeamBannedAbility(aiData->abilities[battlerDef]) || aiData->holdEffects[battlerAtk] == HOLD_EFFECT_ABILITY_SHIELD) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_CORE_ENFORCER: break; @@ -2206,50 +2222,50 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (aiData->abilities[battlerDef] == ABILITY_SIMPLE || IsEntrainmentTargetOrSimpleBeamBannedAbility(aiData->abilities[battlerDef]) || aiData->holdEffects[battlerDef] == HOLD_EFFECT_ABILITY_SHIELD) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SNATCH: if (!HasSnatchAffectedMove(battlerDef) || PartnerHasSameMoveEffectWithoutTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_POWER_TRICK: if (IS_TARGETING_PARTNER(battlerAtk, battlerDef)) - score -= 10; + ADJUST_SCORE(-10); else if (gBattleMons[battlerAtk].defense >= gBattleMons[battlerAtk].attack && !HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_POWER_SWAP: // Don't use if attacker's stat stages are higher than opponents if (IS_TARGETING_PARTNER(battlerAtk, battlerDef)) - score -= 10; + ADJUST_SCORE(-10); else if (gBattleMons[battlerAtk].statStages[STAT_ATK] >= gBattleMons[battlerDef].statStages[STAT_ATK] && gBattleMons[battlerAtk].statStages[STAT_SPATK] >= gBattleMons[battlerDef].statStages[STAT_SPATK]) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_GUARD_SWAP: // Don't use if attacker's stat stages are higher than opponents if (IS_TARGETING_PARTNER(battlerAtk, battlerDef)) - score -= 10; + ADJUST_SCORE(-10); else if (gBattleMons[battlerAtk].statStages[STAT_DEF] >= gBattleMons[battlerDef].statStages[STAT_DEF] && gBattleMons[battlerAtk].statStages[STAT_SPDEF] >= gBattleMons[battlerDef].statStages[STAT_SPDEF]) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SPEED_SWAP: if (IS_TARGETING_PARTNER(battlerAtk, battlerDef)) { - score -= 10; + ADJUST_SCORE(-10); } else { if (gFieldStatuses & STATUS_FIELD_TRICK_ROOM && (gBattleMons[battlerAtk].speed <= gBattleMons[battlerDef].speed)) - score -= 10; + ADJUST_SCORE(-10); else if (gBattleMons[battlerAtk].speed >= gBattleMons[battlerDef].speed) - score -= 10; + ADJUST_SCORE(-10); } break; case EFFECT_HEART_SWAP: if (IS_TARGETING_PARTNER(battlerAtk, battlerDef)) { - score -= 10; + ADJUST_SCORE(-10); } else { @@ -2259,14 +2275,14 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) u32 defNegativeStages = CountNegativeStatStages(battlerDef); if (atkPositiveStages >= defPositiveStages && atkNegativeStages <= defNegativeStages) - score -= 10; + ADJUST_SCORE(-10); break; } break; case EFFECT_POWER_SPLIT: if (IS_TARGETING_PARTNER(battlerAtk, battlerDef)) { - score -= 10; + ADJUST_SCORE(-10); } else { @@ -2276,14 +2292,14 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) u32 defSpAttack = gBattleMons[battlerDef].spAttack; if (atkAttack + atkSpAttack >= defAttack + defSpAttack) // Combined attacker stats are > than combined target stats - score -= 10; + ADJUST_SCORE(-10); break; } break; case EFFECT_GUARD_SPLIT: if (IS_TARGETING_PARTNER(battlerAtk, battlerDef)) { - score -= 10; + ADJUST_SCORE(-10); } else { @@ -2293,7 +2309,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) u32 defSpDefense = gBattleMons[battlerDef].spDefense; if (atkDefense + atkSpDefense >= defDefense + defSpDefense) //Combined attacker stats are > than combined target stats - score -= 10; + ADJUST_SCORE(-10); break; } break; @@ -2301,36 +2317,36 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (predictedMove != MOVE_NONE) { if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_SLOWER) - score -= 10; // Target is predicted to go first, Me First will fail + ADJUST_SCORE(-10); // Target is predicted to go first, Me First will fail else return AI_CheckBadMove(battlerAtk, battlerDef, predictedMove, score); } else { - score -= 10; //Target is predicted to switch most likely + ADJUST_SCORE(-10); //Target is predicted to switch most likely } break; case EFFECT_NATURAL_GIFT: if (aiData->abilities[battlerAtk] == ABILITY_KLUTZ || gFieldStatuses & STATUS_FIELD_MAGIC_ROOM || GetPocketByItemId(gBattleMons[battlerAtk].item) != POCKET_BERRIES) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_GRASSY_TERRAIN: if (PartnerMoveEffectIsTerrain(BATTLE_PARTNER(battlerAtk), aiData->partnerMove) || gFieldStatuses & STATUS_FIELD_GRASSY_TERRAIN) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_ELECTRIC_TERRAIN: if (PartnerMoveEffectIsTerrain(BATTLE_PARTNER(battlerAtk), aiData->partnerMove) || gFieldStatuses & STATUS_FIELD_ELECTRIC_TERRAIN) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_PSYCHIC_TERRAIN: if (PartnerMoveEffectIsTerrain(BATTLE_PARTNER(battlerAtk), aiData->partnerMove) || gFieldStatuses & STATUS_FIELD_PSYCHIC_TERRAIN) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_MISTY_TERRAIN: if (PartnerMoveEffectIsTerrain(BATTLE_PARTNER(battlerAtk), aiData->partnerMove) || gFieldStatuses & STATUS_FIELD_MISTY_TERRAIN) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_PLEDGE: if (isDoubleBattle && gBattleMons[BATTLE_PARTNER(battlerAtk)].hp > 0) @@ -2341,50 +2357,50 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { if (gBattleMons[BATTLE_PARTNER(battlerAtk)].status1 & (STATUS1_SLEEP | STATUS1_FREEZE)) // && gBattleMons[BATTLE_PARTNER(battlerAtk)].status1 != 1) // Will wake up this turn - how would AI know - score -= 10; // Don't use combo move if your partner will cause failure + ADJUST_SCORE(-10); // Don't use combo move if your partner will cause failure } } break; case EFFECT_TRICK_ROOM: if (PartnerMoveIs(BATTLE_PARTNER(battlerAtk), aiData->partnerMove, MOVE_TRICK_ROOM)) { - score -= 10; + ADJUST_SCORE(-10); } else if (gFieldStatuses & STATUS_FIELD_TRICK_ROOM) // Trick Room Up { if (GetBattlerSideSpeedAverage(battlerAtk) < GetBattlerSideSpeedAverage(battlerDef)) // Attacker side slower than target side - score -= 10; // Keep the Trick Room up + ADJUST_SCORE(-10); // Keep the Trick Room up } else { if (GetBattlerSideSpeedAverage(battlerAtk) >= GetBattlerSideSpeedAverage(battlerDef)) // Attacker side faster than target side - score -= 10; // Keep the Trick Room down + ADJUST_SCORE(-10); // Keep the Trick Room down } break; case EFFECT_MAGIC_ROOM: if (gFieldStatuses & STATUS_FIELD_MAGIC_ROOM || PartnerMoveIsSameNoTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_WONDER_ROOM: if (gFieldStatuses & STATUS_FIELD_WONDER_ROOM || PartnerMoveIsSameNoTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_GRAVITY: if ((gFieldStatuses & STATUS_FIELD_GRAVITY && !IS_BATTLER_OF_TYPE(battlerAtk, TYPE_FLYING) && aiData->holdEffects[battlerAtk] != HOLD_EFFECT_AIR_BALLOON) // Should revert Gravity in this case || PartnerMoveIsSameNoTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_ION_DELUGE: if (gFieldStatuses & STATUS_FIELD_ION_DELUGE || PartnerMoveIsSameNoTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_FLING: if (!CanFling(battlerAtk)) { - score -= 10; + ADJUST_SCORE(-10); } else { @@ -2394,24 +2410,24 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { case MOVE_EFFECT_BURN: if (!AI_CanBurn(battlerAtk, battlerDef, BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case MOVE_EFFECT_PARALYSIS: if (!AI_CanParalyze(battlerAtk, battlerDef, aiData->abilities[battlerDef], move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case MOVE_EFFECT_POISON: if (!AI_CanPoison(battlerAtk, battlerDef, aiData->abilities[battlerDef], move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case MOVE_EFFECT_TOXIC: if (!AI_CanPoison(battlerAtk, battlerDef, aiData->abilities[battlerDef], move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case MOVE_EFFECT_FREEZE: if (!CanBeFrozen(battlerDef, TRUE) || MoveBlockedBySubstitute(move, battlerAtk, battlerDef)) - score -= 10; + ADJUST_SCORE(-10); break; }*/ } @@ -2421,12 +2437,12 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) || gFieldStatuses & STATUS_FIELD_MAGIC_ROOM || gDisableStructs[battlerDef].embargoTimer != 0 || PartnerMoveIsSameAsAttacker(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_POWDER: if (!HasMoveWithType(battlerDef, TYPE_FIRE) || PartnerMoveIsSameAsAttacker(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_TELEKINESIS: if (gStatuses3[battlerDef] & (STATUS3_TELEKINESIS | STATUS3_ROOTED | STATUS3_SMACKED_DOWN) @@ -2434,39 +2450,39 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) || aiData->holdEffects[battlerDef] == HOLD_EFFECT_IRON_BALL || IsTelekinesisBannedSpecies(gBattleMons[battlerDef].species) || PartnerMoveIsSameAsAttacker(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_THROAT_CHOP: break; case EFFECT_HEAL_BLOCK: if (gDisableStructs[battlerDef].healBlockTimer != 0 || PartnerMoveIsSameAsAttacker(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SOAK: if (PartnerMoveIsSameAsAttacker(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove) || (gBattleMons[battlerDef].type1 == TYPE_WATER && gBattleMons[battlerDef].type2 == TYPE_WATER && gBattleMons[battlerDef].type3 == TYPE_MYSTERY)) - score -= 10; // target is already water-only + ADJUST_SCORE(-10); // target is already water-only break; case EFFECT_THIRD_TYPE: switch (move) { case MOVE_TRICK_OR_TREAT: if (IS_BATTLER_OF_TYPE(battlerDef, TYPE_GHOST) || PartnerMoveIsSameAsAttacker(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case MOVE_FORESTS_CURSE: if (IS_BATTLER_OF_TYPE(battlerDef, TYPE_GRASS) || PartnerMoveIsSameAsAttacker(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; } break; case EFFECT_HEAL_PULSE: // and floral healing if (!IS_TARGETING_PARTNER(battlerAtk, battlerDef)) // Don't heal enemies { - score -= 10; + ADJUST_SCORE(-10); break; } // fallthrough @@ -2476,16 +2492,16 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (gStatuses3[battlerDef] & STATUS3_HEAL_BLOCK) return 0; if (AtMaxHp(battlerDef)) - score -= 10; + ADJUST_SCORE(-10); else if (gBattleMons[battlerDef].hp > gBattleMons[battlerDef].maxHP / 2) - score -= 5; + ADJUST_SCORE(-5); } break; case EFFECT_ELECTRIFY: if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_SLOWER //|| GetMoveTypeSpecial(battlerDef, predictedMove) == TYPE_ELECTRIC // Move will already be electric type || PartnerMoveIsSameAsAttacker(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_TOPSY_TURVY: if (!IS_TARGETING_PARTNER(battlerAtk, battlerDef)) @@ -2495,18 +2511,18 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (targetPositiveStages == 0 //No good stat changes to make bad || PartnerMoveIsSameAsAttacker(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); else if (targetNegativeStages < targetPositiveStages) - score -= 5; //More stages would be made positive than negative + ADJUST_SCORE(-5); //More stages would be made positive than negative } break; case EFFECT_FAIRY_LOCK: if ((gFieldStatuses & STATUS_FIELD_FAIRY_LOCK) || PartnerMoveIsSameNoTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_DO_NOTHING: - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_INSTRUCT: { @@ -2525,12 +2541,12 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) || gBattleMons[battlerDef].status2 & STATUS2_MULTIPLETURNS || PartnerMoveIsSameAsAttacker(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) { - score -= 10; + ADJUST_SCORE(-10); } else if (isDoubleBattle) { if (!IS_TARGETING_PARTNER(battlerAtk, battlerDef)) - score -= 10; + ADJUST_SCORE(-10); } else { @@ -2541,9 +2557,9 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) | MOVE_TARGET_FOES_AND_ALLY | MOVE_TARGET_OPPONENTS_FIELD) && instructedMove != MOVE_MIND_BLOWN && instructedMove != MOVE_STEEL_BEAM) - score -= 10; //Don't force the enemy to attack you again unless it can kill itself with Mind Blown + ADJUST_SCORE(-10); //Don't force the enemy to attack you again unless it can kill itself with Mind Blown else if (instructedMove != MOVE_MIND_BLOWN) - score -= 5; //Do something better + ADJUST_SCORE(-5); //Do something better } } break; @@ -2551,32 +2567,32 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (!isDoubleBattle || AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_SLOWER || PartnerMoveIsSameAsAttacker(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_AFTER_YOU: if (!IS_TARGETING_PARTNER(battlerAtk, battlerDef) || !isDoubleBattle || AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_SLOWER || PartnerMoveIsSameAsAttacker(BATTLE_PARTNER(battlerAtk), battlerDef, move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SUCKER_PUNCH: if (predictedMove != MOVE_NONE) { if (IS_MOVE_STATUS(predictedMove) || AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_SLOWER) // Opponent going first - score -= 10; + ADJUST_SCORE(-10); } break; case EFFECT_TAILWIND: if (gSideTimers[GetBattlerSide(battlerAtk)].tailwindTimer != 0 || PartnerMoveIs(BATTLE_PARTNER(battlerAtk), aiData->partnerMove, MOVE_TAILWIND) || (gFieldStatuses & STATUS_FIELD_TRICK_ROOM && gFieldTimers.trickRoomTimer > 1)) // Trick Room active and not ending this turn - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_LUCKY_CHANT: if (gSideTimers[GetBattlerSide(battlerAtk)].luckyChantTimer != 0 || PartnerMoveIsSameNoTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_MAGNET_RISE: if (gFieldStatuses & STATUS_FIELD_GRAVITY @@ -2584,15 +2600,15 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) || aiData->holdEffects[battlerAtk] == HOLD_EFFECT_IRON_BALL || gStatuses3[battlerAtk] & (STATUS3_ROOTED | STATUS3_MAGNET_RISE | STATUS3_SMACKED_DOWN) || !IsBattlerGrounded(battlerAtk)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_CAMOUFLAGE: if (!CanCamouflage(battlerAtk)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_LAST_RESORT: if (!CanUseLastResort(battlerAtk)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_SYNCHRONOISE: //Check holding ring target or is of same type @@ -2602,22 +2618,22 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) || IS_BATTLER_OF_TYPE(battlerDef, gBattleMons[battlerAtk].type3)) break; else - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_ERUPTION: if (effectiveness <= AI_EFFECTIVENESS_x0_5) - score--; + ADJUST_SCORE(-1); if (aiData->hpPercents[battlerDef] < 50) - score--; + ADJUST_SCORE(-1); break; case EFFECT_VITAL_THROW: if (AI_STRIKES_FIRST(battlerAtk, battlerDef, move) && aiData->hpPercents[battlerAtk] < 40) - score--; // don't want to move last + ADJUST_SCORE(-1); // don't want to move last break; case EFFECT_FLAIL: if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_SLOWER // Opponent should go first || aiData->hpPercents[battlerAtk] > 50) - score -= 4; + ADJUST_SCORE(-4); break; //TODO //case EFFECT_PLASMA_FISTS: @@ -2628,39 +2644,39 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) //break; case EFFECT_SKY_DROP: if (IS_BATTLER_OF_TYPE(battlerDef, TYPE_FLYING)) - score -= 10; + ADJUST_SCORE(-10); if (BattlerWillFaintFromWeather(battlerAtk, aiData->abilities[battlerAtk]) || DoesSubstituteBlockMove(battlerAtk, battlerDef, move) || GetBattlerWeight(battlerDef) >= 2000) //200.0 kg - score -= 10; + ADJUST_SCORE(-10); break; /*case EFFECT_NO_RETREAT: if (TrappedByNoRetreat(battlerAtk)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_EXTREME_EVOBOOST: if (MainStatsMaxed(battlerAtk)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_CLANGOROUS_SOUL: if (gBattleMons[battlerAtk].hp <= gBattleMons[battlerAtk].maxHP / 3) - score -= 10; + ADJUST_SCORE(-10); break;*/ case EFFECT_REVIVAL_BLESSING: if (GetFirstFaintedPartyIndex(battlerAtk) == PARTY_SIZE) - score -= 10; + ADJUST_SCORE(-10); else if (CanAIFaintTarget(battlerAtk, battlerDef, 0)) - score -= 10; + ADJUST_SCORE(-10); else if (CanTargetFaintAi(battlerDef, battlerAtk) && AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_SLOWER) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_JUNGLE_HEALING: if (AtMaxHp(battlerAtk) && AtMaxHp(BATTLE_PARTNER(battlerAtk)) && !(gBattleMons[battlerAtk].status1 & STATUS1_ANY) && !(gBattleMons[BATTLE_PARTNER(battlerAtk)].status1 & STATUS1_ANY)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_TAKE_HEART: if ((!(gBattleMons[battlerAtk].status1 & STATUS1_ANY) @@ -2669,7 +2685,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) || PartnerMoveIs(BATTLE_PARTNER(battlerAtk), aiData->partnerMove, MOVE_AROMATHERAPY)) && !BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPATK) && !BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPDEF)) - score -= 10; + ADJUST_SCORE(-10); break; case EFFECT_PLACEHOLDER: return 0; // cannot even select @@ -2697,43 +2713,23 @@ static s32 AI_TryToFaint(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { // this move can faint the target if (aiFaster || GetMovePriority(battlerAtk, move) > 0) - score += 5; // we go first or we're using priority move + ADJUST_SCORE(5); // we go first or we're using priority move else - score += 4; + ADJUST_SCORE(4); } else { - // this move isn't expected to faint the target - if (gBattleMoves[move].highCritRatio) - score += 2; // crit makes it more likely to make them faint - if (GetMoveDamageResult(battlerAtk, battlerDef, movesetIndex) == MOVE_POWER_OTHER) - score--; - - switch (AI_DATA->effectiveness[battlerAtk][battlerDef][movesetIndex]) - { - case AI_EFFECTIVENESS_x8: - score += 8; - break; - case AI_EFFECTIVENESS_x4: - score += 4; - break; - case AI_EFFECTIVENESS_x2: - if (AI_RandLessThan(176)) - score += 2; - else - score++; - break; - } + ADJUST_SCORE(-1); } //AI_TryToFaint_CheckIfDanger if (!aiFaster && CanTargetFaintAi(battlerDef, battlerAtk)) { // AI_TryToFaint_Danger if (GetMoveDamageResult(battlerAtk, battlerDef, movesetIndex) != MOVE_POWER_BEST) - score--; + ADJUST_SCORE(-1); else - score++; + ADJUST_SCORE(1); } return score; @@ -2765,14 +2761,14 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) switch (gBattleMoves[aiData->partnerMove].effect) { case EFFECT_HELPING_HAND: - if (!IS_MOVE_STATUS(move)) - score += 5; + if (IS_MOVE_STATUS(move)) + ADJUST_SCORE(-7); break; case EFFECT_PERISH_SONG: if (!(gBattleMons[battlerDef].status2 & (STATUS2_ESCAPE_PREVENTION | STATUS2_WRAPPED))) { if (IsTrappingMoveEffect(effect) || predictedMove == MOVE_INGRAIN) - score++; + ADJUST_SCORE(1); } break; case EFFECT_ALWAYS_CRIT: @@ -2783,13 +2779,22 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { // discourage raising our attack since it's about to be maxed out if (IsAttackBoostMoveEffect(effect)) - score -= 3; + ADJUST_SCORE(-3); // encourage moves hitting multiple opponents if (!IS_MOVE_STATUS(move) && (moveTarget & (MOVE_TARGET_BOTH | MOVE_TARGET_FOES_AND_ALLY))) - score += 3; + ADJUST_SCORE(3); } } break; + // Don't change weather if ally already decided to do so. + case EFFECT_SUNNY_DAY: + case EFFECT_HAIL: + case EFFECT_SNOWSCAPE: + case EFFECT_RAIN_DANCE: + case EFFECT_SANDSTORM: + if (IsMoveEffectWeather(move)) + ADJUST_SCORE(-10); + break; } } // check partner move effect @@ -2797,12 +2802,12 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) switch (effect) { case EFFECT_HELPING_HAND: - if (aiData->partnerMove != 0 && !HasDamagingMove(battlerAtkPartner)) - score -= 5; + if (!IsBattlerAlive(battlerAtkPartner) || !HasDamagingMove(battlerAtkPartner)) + ADJUST_SCORE(-5); break; case EFFECT_PERISH_SONG: if (aiData->partnerMove != 0 && HasTrappingMoveEffect(battlerAtkPartner)) - score++; + ADJUST_SCORE(1); break; case EFFECT_MAGNET_RISE: if (IsBattlerGrounded(battlerAtk) @@ -3082,7 +3087,7 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) } // attacker move effects } // check partner protecting - score -= 30; // otherwise, don't target partner + ADJUST_SCORE(-30); // otherwise, don't target partner } else // checking opponent { @@ -3091,9 +3096,9 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { case EFFECT_SKILL_SWAP: if (aiData->abilities[battlerAtk] == ABILITY_TRUANT) - score += 5; + ADJUST_SCORE(5); else if (IsAbilityOfRating(aiData->abilities[battlerAtk], 0) || IsAbilityOfRating(aiData->abilities[battlerDef], 10)) - score += 2; // we want to transfer our bad ability or take their awesome ability + ADJUST_SCORE(2); // we want to transfer our bad ability or take their awesome ability break; case EFFECT_EARTHQUAKE: case EFFECT_MAGNITUDE: @@ -3101,14 +3106,14 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) || (IsBattlerGrounded(battlerAtkPartner) && AI_WhoStrikesFirst(battlerAtk, battlerAtkPartner, move) == AI_IS_SLOWER && IsUngroundingEffect(gBattleMoves[aiData->partnerMove].effect))) - score += 2; + ADJUST_SCORE(2); else if (IS_BATTLER_OF_TYPE(battlerAtkPartner, TYPE_FIRE) || IS_BATTLER_OF_TYPE(battlerAtkPartner, TYPE_ELECTRIC) || IS_BATTLER_OF_TYPE(battlerAtkPartner, TYPE_POISON) || IS_BATTLER_OF_TYPE(battlerAtkPartner, TYPE_ROCK)) - score -= 10; // partner will be hit by earthquake and is weak to it + ADJUST_SCORE(-10); // partner will be hit by earthquake and is weak to it else if (IsBattlerAlive(battlerAtkPartner)) - score -= 3; + ADJUST_SCORE(-3); break; } @@ -3165,8 +3170,10 @@ static u32 GetAIMostDamagingMoveId(u32 battlerAtk, u32 battlerDef) static s32 AI_CompareDamagingMoves(u32 battlerAtk, u32 battlerDef, u32 currId) { u32 i; - u32 noOfHits[MAX_MON_MOVES]; + bool32 multipleBestMoves = FALSE; + s32 noOfHits[MAX_MON_MOVES]; s32 score = 0; + s32 leastHits = 1000, leastHitsId = 0; u16 *moves = GetMovesArray(battlerAtk); bool8 isPowerfulIgnoredEffect[MAX_MON_MOVES]; @@ -3175,74 +3182,54 @@ static s32 AI_CompareDamagingMoves(u32 battlerAtk, u32 battlerDef, u32 currId) if (moves[i] != MOVE_NONE && gBattleMoves[moves[i]].power) { noOfHits[i] = GetNoOfHitsToKOBattler(battlerAtk, battlerDef, i); + if (noOfHits[i] < leastHits) + { + leastHits = noOfHits[i]; + leastHitsId = i; + } isPowerfulIgnoredEffect[i] = IsInIgnoredPowerfulMoveEffects(gBattleMoves[moves[i]].effect); } else { - noOfHits[i] = 0; + noOfHits[i] = -1; isPowerfulIgnoredEffect[i] = FALSE; } + /* + MgbaPrintf_("%S: required hits: %d Dmg: %d", gMoveNames[moves[i]], noOfHits[i], AI_DATA->simulatedDmg[battlerAtk][battlerDef][i]); + */ } - // if multiple moves can 0HKO, then compare the current move with the others - if (noOfHits[currId] == 1) + // Priority list: + // 1. Less no of hits to ko + // 2. Not in the powerful but ignored move effects table + // 3. More accuracy + // 4. Better effect + + // Current move requires the least hits to KO. Compare with other moves. + if (leastHits == noOfHits[currId]) { for (i = 0; i < MAX_MON_MOVES; i++) { - if (noOfHits[i] != 1 || i == currId) + if (i == currId) continue; - - // prioritize moves without a risky secondary effect such as Overheat, Explosion or Hyper Beam - if (!isPowerfulIgnoredEffect[currId] && isPowerfulIgnoredEffect[i]) - score += 2; - else if (isPowerfulIgnoredEffect[currId] && !isPowerfulIgnoredEffect[i]) - score -= 2; - - if (!isPowerfulIgnoredEffect[currId]) + if (noOfHits[currId] == noOfHits[i]) { - // prioritize moves with higher accuracy - switch (CompareMoveAccuracies(battlerAtk, battlerDef, currId, i)) - { - case 0: - score++; - break; - case 1: - score--; - break; - } - - // prioritize moves without recoil - if (AI_IsDamagedByRecoil(battlerAtk)) + multipleBestMoves = TRUE; + // We need to make sure it's the current move which is objectively better. + if (isPowerfulIgnoredEffect[i] && !isPowerfulIgnoredEffect[currId]) + ADJUST_SCORE(3); + else if (CompareMoveAccuracies(battlerAtk, battlerDef, currId, i) == 0) + ADJUST_SCORE(2); + else if (AI_WhichMoveBetter(moves[currId], moves[i], battlerAtk, battlerDef, noOfHits[currId]) == 0) { - if (!IS_MOVE_RECOIL(moves[currId]) && IS_MOVE_RECOIL(moves[i])) - score++; - else if (IS_MOVE_RECOIL(moves[currId]) && !IS_MOVE_RECOIL(moves[i])) - score--; + // MgbaPrintf_("%S better than %S", gMoveNames[moves[currId]], gMoveNames[moves[i]]); + ADJUST_SCORE(1); } } } - } - else - { - // give priority to moves which can ko the target faster - for (i = 0; i < MAX_MON_MOVES; i++) - { - if (i != currId && noOfHits[i] < 5 && noOfHits[currId] != 1 && noOfHits[i] < noOfHits[currId]) - { - if (AI_WhichMoveBetter(moves[currId], moves[i], battlerAtk, battlerDef) != 0) - score--; - } - } - - // If 2 moves can KO the target in the same number of turns, - // but one of them always hits and there is a risk the other - // move could miss, prioritize the move with better accuracy. - for (i = 0; i < MAX_MON_MOVES; i++) - { - if (i != currId && noOfHits[currId] == noOfHits[i] - && CompareMoveAccuracies(battlerAtk, battlerDef, currId, i)) - score++; - } + // Turns out the current move deals the most dmg compared to the other 3. + if (!multipleBestMoves) + ADJUST_SCORE(1); } return score; @@ -3274,42 +3261,26 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score // check always hits if (!IS_MOVE_STATUS(move) && gBattleMoves[move].accuracy == 0) { - // If 2 moves can KO the target in the same number of turns, but one of them always hits and there is a risk the other move could miss, prioritize the always hits move. - if (gBattleMons[battlerDef].statStages[STAT_EVASION] > 6 || gBattleMons[battlerAtk].statStages[STAT_ACC] < 6) - { - u32 mostDmgMoveId = GetAIMostDamagingMoveId(battlerAtk, battlerDef); - u32 *dmgs = aiData->simulatedDmg[battlerAtk][battlerDef]; - if (GetNoOfHitsToKO(dmgs[mostDmgMoveId], gBattleMons[battlerDef].hp) == GetNoOfHitsToKO(dmgs[movesetIndex], gBattleMons[battlerDef].hp)) - score++; - } if (gBattleMons[battlerDef].statStages[STAT_EVASION] >= 10 || gBattleMons[battlerAtk].statStages[STAT_ACC] <= 2) - score++; + ADJUST_SCORE(1); if (AI_RandLessThan(100) && (gBattleMons[battlerDef].statStages[STAT_EVASION] >= 8 || gBattleMons[battlerAtk].statStages[STAT_ACC] <= 4)) - score++; + ADJUST_SCORE(1); } - // check high crit - if (gBattleMoves[move].highCritRatio && effectiveness >= AI_EFFECTIVENESS_x2 && AI_RandLessThan(128)) - score++; - // check already dead if (!IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) && CanTargetFaintAi(battlerAtk, battlerDef) && AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_SLOWER) // Opponent should go first { if (atkPriority > 0) - score++; + ADJUST_SCORE(1); else - score--; + ADJUST_SCORE(-1); } - // check damage - if (gBattleMoves[move].power != 0 && GetMoveDamageResult(battlerAtk, battlerDef, movesetIndex) == MOVE_POWER_WEAK) - score--; - // check status move preference if (AI_THINKING_STRUCT->aiFlags & AI_FLAG_PREFER_STATUS_MOVES && IS_MOVE_STATUS(move) && effectiveness != AI_EFFECTIVENESS_x0) - score++; + ADJUST_SCORE(1); // check thawing moves if ((gBattleMons[battlerAtk].status1 & (STATUS1_FREEZE | STATUS1_FROSTBITE)) && gBattleMoves[move].thawsUser) @@ -3329,7 +3300,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score break; default: if (IS_MOVE_PHYSICAL(move) && gBattleMoves[move].effect != EFFECT_FACADE) - score -= 2; + ADJUST_SCORE(-2); break; } } @@ -3348,7 +3319,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score break; default: if (IS_MOVE_SPECIAL(move) && gBattleMoves[move].effect != EFFECT_FACADE) - score -= 2; + ADJUST_SCORE(-2); break; } } @@ -3365,7 +3336,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER) // Attacker should go first { if (CanIndexMoveFaintTarget(battlerAtk, battlerDef, movesetIndex, 0)) - score += 8; // prioritize killing target for stat boost + ADJUST_SCORE(8); // prioritize killing target for stat boost } break; } // ability checks @@ -3382,16 +3353,16 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score break; case EFFECT_ABSORB: if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_BIG_ROOT) - score++; + ADJUST_SCORE(1); if (effectiveness <= AI_EFFECTIVENESS_x0_5 && AI_RandLessThan(50)) - score -= 3; + ADJUST_SCORE(-3); break; case EFFECT_EXPLOSION: case EFFECT_MEMENTO: if (AI_THINKING_STRUCT->aiFlags & AI_FLAG_WILL_SUICIDE && gBattleMons[battlerDef].statStages[STAT_EVASION] < 7) { if (aiData->hpPercents[battlerAtk] < 50 && AI_RandLessThan(128)) - score++; + ADJUST_SCORE(1); } break; case EFFECT_MIRROR_MOVE: @@ -3404,45 +3375,45 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score case EFFECT_ATTACK_UP_USER_ALLY: if (MovesWithSplitUnusable(battlerAtk, battlerDef, SPLIT_PHYSICAL)) { - score -= 8; + ADJUST_SCORE(-8); break; } else if (gBattleMons[battlerAtk].statStages[STAT_ATK] < 9) { if (aiData->hpPercents[battlerAtk] > 90 && AI_RandLessThan(128)) { - score += 2; + ADJUST_SCORE(2); break; } } if (!AI_RandLessThan(100)) { - score--; + ADJUST_SCORE(-1); } break; case EFFECT_DEFENSE_UP: case EFFECT_DEFENSE_UP_2: case EFFECT_DEFENSE_UP_3: if (!HasMoveWithSplit(battlerDef, SPLIT_PHYSICAL)) - score -= 2; + ADJUST_SCORE(-2); if (aiData->hpPercents[battlerAtk] > 90 && AI_RandLessThan(128)) - score += 2; + ADJUST_SCORE(2); else if (aiData->hpPercents[battlerAtk] > 70 && AI_RandLessThan(200)) break; else if (aiData->hpPercents[battlerAtk] < 40) - score -= 2; + ADJUST_SCORE(-2); break; case EFFECT_SPEED_UP: case EFFECT_SPEED_UP_2: if (!AI_STRIKES_FIRST(battlerAtk, battlerDef, move)) { if (!AI_RandLessThan(70)) - score += 3; + ADJUST_SCORE(3); } else { - score -= 3; + ADJUST_SCORE(-3); } break; case EFFECT_SPECIAL_ATTACK_UP: @@ -3450,159 +3421,159 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score case EFFECT_SPECIAL_ATTACK_UP_3: if (MovesWithSplitUnusable(battlerAtk, battlerDef, SPLIT_SPECIAL)) { - score -= 8; + ADJUST_SCORE(-8); break; } else if (gBattleMons[battlerAtk].statStages[STAT_SPATK] < 9) { if (aiData->hpPercents[battlerAtk] > 90 && AI_RandLessThan(128)) { - score += 2; + ADJUST_SCORE(2); break; } } if (!AI_RandLessThan(100)) { - score--; + ADJUST_SCORE(-1); } break; case EFFECT_SPECIAL_DEFENSE_UP: case EFFECT_SPECIAL_DEFENSE_UP_2: if (!HasMoveWithSplit(battlerDef, SPLIT_SPECIAL)) - score -= 2; + ADJUST_SCORE(-2); if (aiData->hpPercents[battlerAtk] > 90 && AI_RandLessThan(128)) - score += 2; + ADJUST_SCORE(2); else if (aiData->hpPercents[battlerAtk] > 70 && AI_RandLessThan(200)) break; else if (aiData->hpPercents[battlerAtk] < 40) - score -= 2; + ADJUST_SCORE(-2); break; case EFFECT_ACCURACY_UP: case EFFECT_ACCURACY_UP_2: if (gBattleMons[battlerAtk].statStages[STAT_ACC] >= 9 && !AI_RandLessThan(50)) - score -= 2; + ADJUST_SCORE(-2); else if (aiData->hpPercents[battlerAtk] <= 70) - score -= 2; + ADJUST_SCORE(-2); else - score++; + ADJUST_SCORE(1); break; case EFFECT_EVASION_UP: case EFFECT_EVASION_UP_2: if (aiData->hpPercents[battlerAtk] > 90 && !AI_RandLessThan(100)) - score += 3; + ADJUST_SCORE(3); if (gBattleMons[battlerAtk].statStages[STAT_EVASION] > 9 && AI_RandLessThan(128)) - score--; + ADJUST_SCORE(-1); if ((gBattleMons[battlerDef].status1 & STATUS1_PSN_ANY) && aiData->hpPercents[battlerAtk] >= 50 && !AI_RandLessThan(80)) - score += 3; + ADJUST_SCORE(3); if (gStatuses3[battlerDef] & STATUS3_LEECHSEED && !AI_RandLessThan(70)) - score += 3; + ADJUST_SCORE(3); if (gStatuses3[battlerAtk] & STATUS3_ROOTED && AI_RandLessThan(128)) - score += 2; + ADJUST_SCORE(2); if (gBattleMons[battlerDef].status2 & STATUS2_CURSED && !AI_RandLessThan(70)) - score += 3; + ADJUST_SCORE(3); if (aiData->hpPercents[battlerAtk] < 70 || gBattleMons[battlerAtk].statStages[STAT_EVASION] == DEFAULT_STAT_STAGE) break; else if (aiData->hpPercents[battlerAtk] < 40 || aiData->hpPercents[battlerDef] < 40) - score -= 2; + ADJUST_SCORE(-2); else if (!AI_RandLessThan(70)) - score -= 2; + ADJUST_SCORE(-2); break; // stat lowering effects case EFFECT_ATTACK_DOWN: case EFFECT_ATTACK_DOWN_2: if (!ShouldLowerAttack(battlerAtk, battlerDef, aiData->abilities[battlerDef])) - score -= 2; + ADJUST_SCORE(-2); if (gBattleMons[battlerDef].statStages[STAT_ATK] < DEFAULT_STAT_STAGE) - score--; + ADJUST_SCORE(-1); else if (aiData->hpPercents[battlerAtk] <= 90) - score--; + ADJUST_SCORE(-1); if (gBattleMons[battlerDef].statStages[STAT_ATK] > 3 && !AI_RandLessThan(50)) - score -= 2; + ADJUST_SCORE(-2); else if (aiData->hpPercents[battlerDef] < 70) - score -= 2; + ADJUST_SCORE(-2); break; case EFFECT_DEFENSE_DOWN: case EFFECT_DEFENSE_DOWN_2: if (!ShouldLowerDefense(battlerAtk, battlerDef, aiData->abilities[battlerDef])) - score -= 2; + ADJUST_SCORE(-2); if ((aiData->hpPercents[battlerAtk] < 70 && !AI_RandLessThan(50)) || (gBattleMons[battlerDef].statStages[STAT_DEF] <= 3 && !AI_RandLessThan(50))) - score -= 2; + ADJUST_SCORE(-2); if (aiData->hpPercents[battlerDef] <= 70) - score -= 2; + ADJUST_SCORE(-2); break; case EFFECT_SPEED_DOWN: case EFFECT_SPEED_DOWN_2: if (AI_STRIKES_FIRST(battlerAtk, battlerDef, move)) - score -= 3; + ADJUST_SCORE(-3); else if (!AI_RandLessThan(70)) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_SPECIAL_ATTACK_DOWN: case EFFECT_SPECIAL_ATTACK_DOWN_2: if (!ShouldLowerSpAtk(battlerAtk, battlerDef, aiData->abilities[battlerDef])) - score -= 2; + ADJUST_SCORE(-2); if (gBattleMons[battlerDef].statStages[STAT_SPATK] < DEFAULT_STAT_STAGE) - score--; + ADJUST_SCORE(-1); else if (aiData->hpPercents[battlerAtk] <= 90) - score--; + ADJUST_SCORE(-1); if (gBattleMons[battlerDef].statStages[STAT_SPATK] > 3 && !AI_RandLessThan(50)) - score -= 2; + ADJUST_SCORE(-2); else if (aiData->hpPercents[battlerDef] < 70) - score -= 2; + ADJUST_SCORE(-2); break; case EFFECT_SPECIAL_DEFENSE_DOWN: case EFFECT_SPECIAL_DEFENSE_DOWN_2: if (!ShouldLowerSpDef(battlerAtk, battlerDef, aiData->abilities[battlerDef])) - score -= 2; + ADJUST_SCORE(-2); if ((aiData->hpPercents[battlerAtk] < 70 && !AI_RandLessThan(50)) || (gBattleMons[battlerDef].statStages[STAT_SPDEF] <= 3 && !AI_RandLessThan(50))) - score -= 2; + ADJUST_SCORE(-2); if (aiData->hpPercents[battlerDef] <= 70) - score -= 2; + ADJUST_SCORE(-2); break; case EFFECT_ACCURACY_DOWN: case EFFECT_ACCURACY_DOWN_2: if (ShouldLowerAccuracy(battlerAtk, battlerDef, aiData->abilities[battlerDef])) - score -= 2; + ADJUST_SCORE(-2); if ((aiData->hpPercents[battlerAtk] < 70 || aiData->hpPercents[battlerDef] < 70) && AI_RandLessThan(100)) - score--; + ADJUST_SCORE(-1); if (gBattleMons[battlerDef].statStages[STAT_ACC] <= 4 && !AI_RandLessThan(80)) - score -= 2; + ADJUST_SCORE(-2); if (gBattleMons[battlerDef].status1 & STATUS1_PSN_ANY && !AI_RandLessThan(70)) - score += 2; + ADJUST_SCORE(2); if (gStatuses3[battlerDef] & STATUS3_LEECHSEED && !AI_RandLessThan(70)) - score += 2; + ADJUST_SCORE(2); if (gStatuses3[battlerDef] & STATUS3_ROOTED && AI_RandLessThan(128)) - score++; + ADJUST_SCORE(1); if (gBattleMons[battlerDef].status2 & STATUS2_CURSED && !AI_RandLessThan(70)) - score += 2; + ADJUST_SCORE(2); if (aiData->hpPercents[battlerAtk] > 70 || gBattleMons[battlerDef].statStages[STAT_ACC] < DEFAULT_STAT_STAGE) break; else if (aiData->hpPercents[battlerAtk] < 40 || aiData->hpPercents[battlerDef] < 40 || !AI_RandLessThan(70)) - score -= 2; + ADJUST_SCORE(-2); break; case EFFECT_EVASION_DOWN: case EFFECT_EVASION_DOWN_2: if (!ShouldLowerEvasion(battlerAtk, battlerDef, aiData->abilities[battlerDef])) - score -= 2; + ADJUST_SCORE(-2); if ((aiData->hpPercents[battlerAtk] < 70 || gBattleMons[battlerDef].statStages[STAT_EVASION] <= 3) && !AI_RandLessThan(50)) - score -= 2; + ADJUST_SCORE(-2); if (aiData->hpPercents[battlerDef] <= 70) - score -= 2; + ADJUST_SCORE(-2); if (gBattleMons[battlerAtk].statStages[STAT_ACC] < DEFAULT_STAT_STAGE) - score++; + ADJUST_SCORE(1); if (gBattleMons[battlerDef].statStages[STAT_EVASION] < 7 || aiData->abilities[battlerAtk] == ABILITY_NO_GUARD) - score -= 2; + ADJUST_SCORE(-2); break; case EFFECT_BIDE: if (aiData->hpPercents[battlerAtk] < 90) - score -= 2; + ADJUST_SCORE(-2); break; case EFFECT_DREAM_EATER: if (!(gBattleMons[battlerDef].status1 & STATUS1_SLEEP)) break; - score++; // if target is asleep, dream eater is a pretty good move even without draining + ADJUST_SCORE(1); // if target is asleep, dream eater is a pretty good move even without draining // fallthrough case EFFECT_ACUPRESSURE: break; @@ -3623,7 +3594,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score case EFFECT_HAZE: if (AnyStatIsRaised(BATTLE_PARTNER(battlerAtk)) || PartnerHasSameMoveEffectWithoutTarget(BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove)) - score -= 3; + ADJUST_SCORE(-3); break; // fallthrough case EFFECT_ROAR: @@ -3638,11 +3609,11 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score if (AI_MoveMakesContact(aiData->abilities[battlerAtk], aiData->holdEffects[battlerAtk], move) && aiData->abilities[battlerAtk] != ABILITY_MAGIC_GUARD && aiData->holdEffects[battlerDef] == HOLD_EFFECT_ROCKY_HELMET) - score -= 2; + ADJUST_SCORE(-2); break; case EFFECT_CONVERSION: if (!IS_BATTLER_OF_TYPE(battlerAtk, gBattleMoves[gBattleMons[battlerAtk].moves[0]].type)) - score++; + ADJUST_SCORE(1); break; case EFFECT_FLINCH_HIT: score += ShouldTryToFlinch(battlerAtk, battlerDef, aiData->abilities[battlerAtk], aiData->abilities[battlerDef], move); @@ -3671,7 +3642,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score } if (ShouldRecover(battlerAtk, battlerDef, move, healPercent)) - score += 2; + ADJUST_SCORE(2); } break; case EFFECT_RESTORE_HP: @@ -3681,9 +3652,9 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score case EFFECT_SYNTHESIS: case EFFECT_MOONLIGHT: if (ShouldRecover(battlerAtk, battlerDef, move, 50)) - score += 3; + ADJUST_SCORE(3); if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_BIG_ROOT) - score++; + ADJUST_SCORE(1); break; case EFFECT_TOXIC: case EFFECT_POISON: @@ -3696,11 +3667,11 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score case EFFECT_AURORA_VEIL: if (ShouldSetScreen(battlerAtk, battlerDef, moveEffect)) { - score += 5; + ADJUST_SCORE(5); if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_LIGHT_CLAY) - score += 2; + ADJUST_SCORE(2); if (AI_THINKING_STRUCT->aiFlags & AI_FLAG_SCREENER) - score += 2; + ADJUST_SCORE(2); } break; case EFFECT_REST: @@ -3718,17 +3689,17 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score || aiData->abilities[battlerAtk] == ABILITY_EARLY_BIRD || (AI_GetWeather(aiData) & B_WEATHER_RAIN && gWishFutureKnock.weatherDuration != 1 && aiData->abilities[battlerAtk] == ABILITY_HYDRATION && aiData->holdEffects[battlerAtk] != HOLD_EFFECT_UTILITY_UMBRELLA)) { - score += 2; + ADJUST_SCORE(2); } else { - score++; + ADJUST_SCORE(1); } } break; case EFFECT_OHKO: if (gStatuses3[battlerAtk] & STATUS3_ALWAYS_HITS) - score += 5; + ADJUST_SCORE(5); break; case EFFECT_TRAP: if (HasMoveEffect(battlerDef, EFFECT_RAPID_SPIN)) @@ -3738,11 +3709,11 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score if (IsBattlerTrapped(battlerDef, TRUE)) break; // in this case its a bad attacking move else if (ShouldTrap(battlerAtk, battlerDef, move)) - score += 5; + ADJUST_SCORE(5); break; case EFFECT_MIST: if (AI_THINKING_STRUCT->aiFlags & AI_FLAG_SCREENER) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_FOCUS_ENERGY: case EFFECT_LASER_FOCUS: @@ -3750,11 +3721,11 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score || aiData->abilities[battlerAtk] == ABILITY_SNIPER || aiData->holdEffects[battlerAtk] == HOLD_EFFECT_SCOPE_LENS || HasHighCritRatioMove(battlerAtk)) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_CONFUSE_HIT: if (sereneGraceBoost) - score++; + ADJUST_SCORE(1); //fallthrough case EFFECT_CONFUSE: IncreaseConfusionScore(battlerAtk, battlerDef, move, &score); @@ -3764,7 +3735,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score break; case EFFECT_GRAV_APPLE: if (gFieldStatuses & STATUS_FIELD_GRAVITY) - score += 2; + ADJUST_SCORE(2); // fall through case EFFECT_ATTACK_DOWN_HIT: case EFFECT_DEFENSE_DOWN_HIT: @@ -3773,26 +3744,22 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score case EFFECT_ACCURACY_DOWN_HIT: case EFFECT_EVASION_DOWN_HIT: if (sereneGraceBoost && aiData->abilities[battlerDef] != ABILITY_CONTRARY) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_SPEED_DOWN_HIT: - if (AI_STRIKES_FIRST(battlerAtk, battlerDef, move)) - score -= 2; - else if (!AI_RandLessThan(70)) - score++; if (ShouldLowerSpeed(battlerAtk, battlerDef, aiData->abilities[battlerDef])) { if (sereneGraceBoost && aiData->abilities[battlerDef] != ABILITY_CONTRARY) - score += 5; + ADJUST_SCORE(5); else - score += 2; + ADJUST_SCORE(2); } break; case EFFECT_SUBSTITUTE: if (gStatuses3[battlerDef] & STATUS3_PERISH_SONG) - score += 3; + ADJUST_SCORE(3); if (gBattleMons[battlerDef].status1 & (STATUS1_BURN | STATUS1_PSN_ANY | STATUS1_FROSTBITE)) - score++; + ADJUST_SCORE(1); if (HasMoveEffect(battlerDef, EFFECT_SLEEP) || HasMoveEffect(battlerDef, EFFECT_TOXIC) || HasMoveEffect(battlerDef, EFFECT_POISON) @@ -3800,9 +3767,9 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score || HasMoveEffect(battlerDef, EFFECT_WILL_O_WISP) || HasMoveEffect(battlerDef, EFFECT_CONFUSE) || HasMoveEffect(battlerDef, EFFECT_LEECH_SEED)) - score += 2; + ADJUST_SCORE(2); if (!gBattleMons[battlerDef].status2 & (STATUS2_WRAPPED | STATUS2_ESCAPE_PREVENTION && aiData->hpPercents[battlerAtk] > 70)) - score++; + ADJUST_SCORE(1); break; case EFFECT_MIMIC: if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER) @@ -3818,9 +3785,9 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score || aiData->abilities[battlerDef] == ABILITY_LIQUID_OOZE || aiData->abilities[battlerDef] == ABILITY_MAGIC_GUARD) break; - score += 3; + ADJUST_SCORE(3); if (!HasDamagingMove(battlerDef) || IsBattlerTrapped(battlerDef, FALSE)) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_DO_NOTHING: //todo - check z splash, z celebrate, z happy hour (lol) @@ -3836,12 +3803,12 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score switch (ShouldPivot(battlerAtk, battlerDef, aiData->abilities[battlerDef], move, movesetIndex)) { case 0: // no - score -= 10; // technically should go in CheckBadMove, but this is easier/less computationally demanding + ADJUST_SCORE(-10); // technically should go in CheckBadMove, but this is easier/less computationally demanding break; case 1: // maybe break; case 2: // yes - score += 7; + ADJUST_SCORE(7); break; } } @@ -3851,14 +3818,14 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score break; // Can't switch //if (switchAbility == ABILITY_INTIMIDATE && PartyHasMoveSplit(battlerDef, SPLIT_PHYSICAL)) - //score += 7; + //ADJUST_SCORE(7); } break; case EFFECT_BATON_PASS: if (ShouldSwitch(battlerAtk) && (gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE || (gStatuses3[battlerAtk] & (STATUS3_ROOTED | STATUS3_AQUA_RING | STATUS3_MAGNET_RISE | STATUS3_POWER_TRICK)) || AnyStatIsRaised(battlerAtk))) - score += 5; + ADJUST_SCORE(5); break; case EFFECT_DISABLE: if (gDisableStructs[battlerDef].disableTimer == 0 @@ -3873,14 +3840,14 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score && gLastMoves[battlerDef] != 0xFFFF) { if (gLastMoves[battlerDef] == predictedMove) - score += 3; + ADJUST_SCORE(3); else if (CanTargetMoveFaintAi(gLastMoves[battlerDef], battlerDef, battlerAtk, 1)) - score += 2; //Disable move that can kill attacker + ADJUST_SCORE(2); //Disable move that can kill attacker } } else if (predictedMove != MOVE_NONE && IS_MOVE_STATUS(predictedMove)) { - score++; // Disable annoying status moves + ADJUST_SCORE(1); // Disable annoying status moves } } break; @@ -3892,7 +3859,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score ) { if (IsEncoreEncouragedEffect(gBattleMoves[gLastMoves[battlerDef]].effect)) - score += 3; + ADJUST_SCORE(3); } break; case EFFECT_PAIN_SPLIT: @@ -3900,31 +3867,31 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score u32 newHp = (gBattleMons[battlerAtk].hp + gBattleMons[battlerDef].hp) / 2; u32 healthBenchmark = (gBattleMons[battlerAtk].hp * 12) / 10; if (newHp > healthBenchmark && ShouldAbsorb(battlerAtk, battlerDef, move, aiData->simulatedDmg[battlerAtk][battlerDef][movesetIndex])) - score += 2; + ADJUST_SCORE(2); } break; case EFFECT_SLEEP_TALK: case EFFECT_SNORE: if (!IsWakeupTurn(battlerAtk) && gBattleMons[battlerAtk].status1 & STATUS1_SLEEP) - score += 10; + ADJUST_SCORE(10); break; case EFFECT_LOCK_ON: if (HasMoveEffect(battlerAtk, EFFECT_OHKO)) - score += 3; + ADJUST_SCORE(3); else if (aiData->abilities[battlerAtk] == ABILITY_COMPOUND_EYES && HasMoveWithLowAccuracy(battlerAtk, battlerDef, 80, TRUE, aiData->abilities[battlerAtk], aiData->abilities[battlerDef], aiData->holdEffects[battlerAtk], aiData->holdEffects[battlerDef])) - score += 3; + ADJUST_SCORE(3); else if (HasMoveWithLowAccuracy(battlerAtk, battlerDef, 85, TRUE, aiData->abilities[battlerAtk], aiData->abilities[battlerDef], aiData->holdEffects[battlerAtk], aiData->holdEffects[battlerDef])) - score += 3; + ADJUST_SCORE(3); else if (HasMoveWithLowAccuracy(battlerAtk, battlerDef, 90, TRUE, aiData->abilities[battlerAtk], aiData->abilities[battlerDef], aiData->holdEffects[battlerAtk], aiData->holdEffects[battlerDef])) - score++; + ADJUST_SCORE(1); break; case EFFECT_SPEED_UP_HIT: if (sereneGraceBoost && aiData->abilities[battlerDef] != ABILITY_CONTRARY && !AI_STRIKES_FIRST(battlerAtk, battlerDef, move)) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_DESTINY_BOND: if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER && CanTargetFaintAi(battlerDef, battlerAtk)) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_SPITE: //TODO - predicted move @@ -3932,7 +3899,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score case EFFECT_WISH: case EFFECT_HEAL_BELL: if (ShouldUseWishAromatherapy(battlerAtk, battlerDef, move)) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_THIEF: { @@ -3958,29 +3925,29 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score case HOLD_EFFECT_CHOICE_BAND: case HOLD_EFFECT_CHOICE_SCARF: case HOLD_EFFECT_CHOICE_SPECS: - score += 2; + ADJUST_SCORE(2); break; case HOLD_EFFECT_TOXIC_ORB: if (ShouldPoisonSelf(battlerAtk, aiData->abilities[battlerAtk])) - score += 2; + ADJUST_SCORE(2); break; case HOLD_EFFECT_FLAME_ORB: if (ShouldBurnSelf(battlerAtk, aiData->abilities[battlerAtk])) - score += 2; + ADJUST_SCORE(2); break; case HOLD_EFFECT_BLACK_SLUDGE: if (IS_BATTLER_OF_TYPE(battlerAtk, TYPE_POISON)) - score += 2; + ADJUST_SCORE(2); break; case HOLD_EFFECT_IRON_BALL: if (HasMoveEffect(battlerAtk, EFFECT_FLING)) - score += 2; + ADJUST_SCORE(2); break; case HOLD_EFFECT_LAGGING_TAIL: case HOLD_EFFECT_STICKY_BARB: break; default: - score++; + ADJUST_SCORE(1); break; } } @@ -3992,18 +3959,18 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score && !(gBattleMons[battlerDef].status2 & STATUS2_NIGHTMARE) && AI_IsBattlerAsleepOrComatose(battlerDef)) { - score += 5; + ADJUST_SCORE(5); if (IsBattlerTrapped(battlerDef, TRUE)) - score += 3; + ADJUST_SCORE(3); } break; case EFFECT_CURSE: if (IS_BATTLER_OF_TYPE(battlerAtk, TYPE_GHOST)) { if (IsBattlerTrapped(battlerDef, TRUE)) - score += 3; + ADJUST_SCORE(3); else - score++; + ADJUST_SCORE(1); break; } else @@ -4053,7 +4020,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score && gBattleMons[battlerAtk].species == SPECIES_AEGISLASH_BLADE && !IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef])) { - score += 3; + ADJUST_SCORE(3); break; } //fallthrough @@ -4068,12 +4035,12 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score if (gBattleMons[battlerAtk].hp > gBattleMons[battlerAtk].maxHP / 4 // Pinch berry couldn't have activated yet && IsPinchBerryItemEffect(aiData->holdEffects[battlerAtk])) { - score += 3; + ADJUST_SCORE(3); } else if (gBattleMons[battlerAtk].hp > 1) // Only spam endure for Flail/Reversal if you're not at Min Health { if (HasMoveEffect(battlerAtk, EFFECT_FLAIL) || HasMoveEffect(battlerAtk, EFFECT_ENDEAVOR)) - score += 3; + ADJUST_SCORE(3); } } break; @@ -4086,7 +4053,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score if (aiData->abilities[battlerDef] == ABILITY_MAGIC_BOUNCE || CountUsablePartyMons(battlerDef) == 0) break; if (gDisableStructs[battlerAtk].isFirstTurn) - score += 2; + ADJUST_SCORE(2); //TODO - track entire opponent party data to determine hazard effectiveness break; case EFFECT_FORESIGHT: @@ -4096,27 +4063,27 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score || (IS_BATTLER_OF_TYPE(battlerDef, TYPE_GHOST) && (HasMoveWithType(battlerAtk, TYPE_NORMAL) || HasMoveWithType(battlerAtk, TYPE_FIGHTING)))) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_MIRACLE_EYE: if (gBattleMons[battlerDef].statStages[STAT_EVASION] > DEFAULT_STAT_STAGE || (IS_BATTLER_OF_TYPE(battlerDef, TYPE_DARK) && (HasMoveWithType(battlerAtk, TYPE_PSYCHIC)))) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_PERISH_SONG: if (IsBattlerTrapped(battlerDef, TRUE)) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_SANDSTORM: if (ShouldSetSandstorm(battlerAtk, aiData->holdEffects[battlerAtk], aiData->holdEffects[battlerAtk])) { - score++; + ADJUST_SCORE(1); if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_SMOOTH_ROCK) - score++; + ADJUST_SCORE(1); if (HasMoveEffect(battlerDef, EFFECT_MORNING_SUN) || HasMoveEffect(battlerDef, EFFECT_SYNTHESIS) || HasMoveEffect(battlerDef, EFFECT_MOONLIGHT)) - score += 2; + ADJUST_SCORE(2); } break; case EFFECT_HAIL: @@ -4124,15 +4091,15 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score { if ((HasMoveEffect(battlerAtk, EFFECT_AURORA_VEIL) || HasMoveEffect(BATTLE_PARTNER(battlerAtk), EFFECT_AURORA_VEIL)) && ShouldSetScreen(battlerAtk, battlerDef, EFFECT_AURORA_VEIL)) - score += 3; + ADJUST_SCORE(3); - score++; + ADJUST_SCORE(1); if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_ICY_ROCK) - score++; + ADJUST_SCORE(1); if (HasMoveEffect(battlerDef, EFFECT_MORNING_SUN) || HasMoveEffect(battlerDef, EFFECT_SYNTHESIS) || HasMoveEffect(battlerDef, EFFECT_MOONLIGHT)) - score += 2; + ADJUST_SCORE(2); } break; case EFFECT_SNOWSCAPE: @@ -4140,42 +4107,42 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score { if ((HasMoveEffect(battlerAtk, EFFECT_AURORA_VEIL) || HasMoveEffect(BATTLE_PARTNER(battlerAtk), EFFECT_AURORA_VEIL)) && ShouldSetScreen(battlerAtk, battlerDef, EFFECT_AURORA_VEIL)) - score += 3; + ADJUST_SCORE(3); - score++; + ADJUST_SCORE(1); if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_ICY_ROCK) - score++; + ADJUST_SCORE(1); if (HasMoveEffect(battlerDef, EFFECT_MORNING_SUN) || HasMoveEffect(battlerDef, EFFECT_SYNTHESIS) || HasMoveEffect(battlerDef, EFFECT_MOONLIGHT)) - score += 2; + ADJUST_SCORE(2); } break; case EFFECT_RAIN_DANCE: if (ShouldSetRain(battlerAtk, aiData->abilities[battlerAtk], aiData->holdEffects[battlerAtk])) { - score++; + ADJUST_SCORE(1); if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_DAMP_ROCK) - score++; + ADJUST_SCORE(1); if (HasMoveEffect(battlerDef, EFFECT_MORNING_SUN) || HasMoveEffect(battlerDef, EFFECT_SYNTHESIS) || HasMoveEffect(battlerDef, EFFECT_SOLAR_BEAM) || HasMoveEffect(battlerDef, EFFECT_MOONLIGHT)) - score += 2; + ADJUST_SCORE(2); if (HasMoveWithType(battlerDef, TYPE_FIRE) || HasMoveWithType(BATTLE_PARTNER(battlerDef), TYPE_FIRE)) - score++; + ADJUST_SCORE(1); } break; case EFFECT_SUNNY_DAY: if (ShouldSetSun(battlerAtk, aiData->abilities[battlerAtk], aiData->holdEffects[battlerAtk])) { - score++; + ADJUST_SCORE(1); if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_HEAT_ROCK) - score++; + ADJUST_SCORE(1); if (HasMoveWithType(battlerDef, TYPE_WATER) || HasMoveWithType(BATTLE_PARTNER(battlerDef), TYPE_WATER)) - score++; + ADJUST_SCORE(1); if (HasMoveEffect(battlerDef, EFFECT_THUNDER) || HasMoveEffect(BATTLE_PARTNER(battlerDef), EFFECT_THUNDER)) - score++; + ADJUST_SCORE(1); } break; case EFFECT_ATTACK_UP_HIT: @@ -4192,9 +4159,9 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score && CanIndexMoveFaintTarget(battlerAtk, battlerDef, movesetIndex, 0)) { if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER) // Attacker goes first - score += 9; + ADJUST_SCORE(9); else - score += 3; + ADJUST_SCORE(3); } break; case EFFECT_BELLY_DRUM: @@ -4212,54 +4179,54 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score { case STAT_ATK: if (HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL)) - score++; + ADJUST_SCORE(1); break; case STAT_SPATK: if (HasMoveWithSplit(battlerAtk, SPLIT_SPECIAL)) - score++; + ADJUST_SCORE(1); break; case STAT_ACC: case STAT_EVASION: case STAT_SPEED: - score++; + ADJUST_SCORE(1); break; case STAT_DEF: case STAT_SPDEF: if (AI_THINKING_STRUCT->aiFlags & AI_FLAG_STALL) - score++; + ADJUST_SCORE(1); break; } } } break; case EFFECT_SEMI_INVULNERABLE: - score++; + ADJUST_SCORE(1); if (predictedMove != MOVE_NONE && !isDoubleBattle) { if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER) // Attacker goes first { if (gBattleMoves[predictedMove].effect == EFFECT_EXPLOSION || gBattleMoves[predictedMove].effect == EFFECT_PROTECT) - score += 3; + ADJUST_SCORE(3); } else if (gBattleMoves[predictedMove].effect == EFFECT_SEMI_INVULNERABLE && !(gStatuses3[battlerDef] & STATUS3_SEMI_INVULNERABLE)) { - score += 3; + ADJUST_SCORE(3); } } break; case EFFECT_DEFENSE_CURL: if (HasMoveEffect(battlerAtk, EFFECT_ROLLOUT) && !(gBattleMons[battlerAtk].status2 & STATUS2_DEFENSE_CURL)) - score++; + ADJUST_SCORE(1); IncreaseStatUpScore(battlerAtk, battlerDef, STAT_DEF, &score); break; case EFFECT_FAKE_OUT: if (move == MOVE_FAKE_OUT) // filter out first impression { if (ShouldFakeOut(battlerAtk, battlerDef, move)) - score += 4; + ADJUST_SCORE(4); else - score -= 10; + ADJUST_SCORE(-10); } break; case EFFECT_STOCKPILE: @@ -4267,43 +4234,43 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score break; if (HasMoveEffect(battlerAtk, EFFECT_SWALLOW) || HasMoveEffect(battlerAtk, EFFECT_SPIT_UP)) - score += 2; + ADJUST_SCORE(2); IncreaseStatUpScore(battlerAtk, battlerDef, STAT_DEF, &score); IncreaseStatUpScore(battlerAtk, battlerDef, STAT_SPDEF, &score); break; case EFFECT_SPIT_UP: if (gDisableStructs[battlerAtk].stockpileCounter >= 2) - score++; + ADJUST_SCORE(1); break; case EFFECT_ROLLOUT: if (gBattleMons[battlerAtk].status2 & STATUS2_DEFENSE_CURL) - score += 8; + ADJUST_SCORE(8); break; case EFFECT_SWAGGER: if (HasMoveEffect(battlerAtk, EFFECT_FOUL_PLAY) || HasMoveEffect(battlerAtk, EFFECT_PSYCH_UP) || HasMoveEffect(battlerAtk, EFFECT_SPECTRAL_THIEF)) - score++; + ADJUST_SCORE(1); if (aiData->abilities[battlerDef] == ABILITY_CONTRARY) - score += 2; + ADJUST_SCORE(2); IncreaseConfusionScore(battlerAtk, battlerDef, move, &score); break; case EFFECT_FLATTER: if (HasMoveEffect(battlerAtk, EFFECT_PSYCH_UP) || HasMoveEffect(battlerAtk, EFFECT_SPECTRAL_THIEF)) - score += 2; + ADJUST_SCORE(2); if (aiData->abilities[battlerDef] == ABILITY_CONTRARY) - score += 2; + ADJUST_SCORE(2); IncreaseConfusionScore(battlerAtk, battlerDef, move, &score); break; case EFFECT_FURY_CUTTER: if (!isDoubleBattle && aiData->holdEffects[battlerAtk] == HOLD_EFFECT_METRONOME) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_ATTRACT: if (!isDoubleBattle && BattlerWillFaintFromSecondaryDamage(battlerDef, aiData->abilities[battlerDef]) @@ -4313,22 +4280,22 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score if (gBattleMons[battlerDef].status1 & STATUS1_ANY || (gBattleMons[battlerDef].status2 & STATUS2_CONFUSION) || IsBattlerTrapped(battlerDef, TRUE)) - score += 2; + ADJUST_SCORE(2); else - score++; + ADJUST_SCORE(1); break; case EFFECT_SAFEGUARD: if (!AI_IsTerrainAffected(battlerAtk, STATUS_FIELD_MISTY_TERRAIN) || !IsBattlerGrounded(battlerAtk)) - score++; + ADJUST_SCORE(1); //if (CountUsablePartyMons(battlerDef) != 0) - //score += 8; + //ADJUST_SCORE(8); break; case EFFECT_PURSUIT: /*TODO if (IsPredictedToSwitch(battlerDef, battlerAtk)) - score += 3; + ADJUST_SCORE(3); else if (IsPredictedToUsePursuitableMove(battlerDef, battlerAtk) && !MoveWouldHitFirst(move, battlerAtk, battlerDef)) //Pursuit against fast U-Turn - score += 3;*/ + ADJUST_SCORE(3);*/ break; case EFFECT_RAPID_SPIN: IncreaseStatUpScore(battlerAtk, battlerDef, STAT_SPEED, &score); // Gen 8 increases speed @@ -4336,7 +4303,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score case EFFECT_DEFOG: if (gSideStatuses[GetBattlerSide(battlerAtk)] & SIDE_STATUS_HAZARDS_ANY && CountUsablePartyMons(battlerAtk) != 0) { - score += 3; + ADJUST_SCORE(3); break; } @@ -4345,7 +4312,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score case MOVE_DEFOG: if (gSideStatuses[GetBattlerSide(battlerDef)] & (SIDE_STATUS_SCREEN_ANY | SIDE_STATUS_SAFEGUARD | SIDE_STATUS_MIST)) { - score += 3; + ADJUST_SCORE(3); } else if (!(gSideStatuses[GetBattlerSide(battlerDef)] & SIDE_STATUS_SPIKES)) //Don't blow away hazards if you set them up { @@ -4361,16 +4328,16 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score { if (gBattleMons[battlerDef].statStages[STAT_EVASION] > 7 || HasMoveWithLowAccuracy(battlerAtk, battlerDef, 90, TRUE, aiData->abilities[battlerAtk], aiData->abilities[battlerDef], aiData->holdEffects[battlerAtk], aiData->holdEffects[battlerDef])) - score += 2; // encourage lowering evasion if they are evasive or we have a move with low accuracy + ADJUST_SCORE(2); // encourage lowering evasion if they are evasive or we have a move with low accuracy else - score++; + ADJUST_SCORE(1); } } break; case MOVE_RAPID_SPIN: case MOVE_MORTAL_SPIN: if (gStatuses3[battlerAtk] & STATUS3_LEECHSEED || gBattleMons[battlerAtk].status2 & STATUS2_WRAPPED) - score += 3; + ADJUST_SCORE(3); break; } break; @@ -4388,58 +4355,58 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score { u32 predictedMoveOnPartner = gLastMoves[BATTLE_PARTNER(battlerAtk)]; if (predictedMoveOnPartner != MOVE_NONE && !IS_MOVE_STATUS(predictedMoveOnPartner)) - score += 3; + ADJUST_SCORE(3); } break; case EFFECT_NATURE_POWER: return AI_CheckViability(battlerAtk, battlerDef, GetNaturePowerMove(), score); case EFFECT_CHARGE: if (HasDamagingMoveOfType(battlerAtk, TYPE_ELECTRIC)) - score += 2; + ADJUST_SCORE(2); #if B_CHARGE_SPDEF_RAISE >= GEN_5 IncreaseStatUpScore(battlerAtk, battlerDef, STAT_SPDEF, &score); #endif break; case EFFECT_TAUNT: if (IS_MOVE_STATUS(predictedMove)) - score += 3; + ADJUST_SCORE(3); else if (HasMoveWithSplit(battlerDef, SPLIT_STATUS)) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_TRICK: case EFFECT_BESTOW: switch (aiData->holdEffects[battlerAtk]) { case HOLD_EFFECT_CHOICE_SCARF: - score += 2; // assume its beneficial + ADJUST_SCORE(2); // assume its beneficial break; case HOLD_EFFECT_CHOICE_BAND: if (!HasMoveWithSplit(battlerDef, SPLIT_PHYSICAL)) - score += 2; + ADJUST_SCORE(2); break; case HOLD_EFFECT_CHOICE_SPECS: if (!HasMoveWithSplit(battlerDef, SPLIT_SPECIAL)) - score += 2; + ADJUST_SCORE(2); break; case HOLD_EFFECT_TOXIC_ORB: if (!ShouldPoisonSelf(battlerAtk, aiData->abilities[battlerAtk])) - score += 2; + ADJUST_SCORE(2); break; case HOLD_EFFECT_FLAME_ORB: if (!ShouldBurnSelf(battlerAtk, aiData->abilities[battlerAtk]) && AI_CanBeBurned(battlerAtk, aiData->abilities[battlerDef])) - score += 2; + ADJUST_SCORE(2); break; case HOLD_EFFECT_BLACK_SLUDGE: if (!IS_BATTLER_OF_TYPE(battlerDef, TYPE_POISON) && aiData->abilities[battlerDef] != ABILITY_MAGIC_GUARD) - score += 3; + ADJUST_SCORE(3); break; case HOLD_EFFECT_IRON_BALL: if (!HasMoveEffect(battlerDef, EFFECT_FLING) || !IsBattlerGrounded(battlerDef)) - score += 2; + ADJUST_SCORE(2); break; case HOLD_EFFECT_LAGGING_TAIL: case HOLD_EFFECT_STICKY_BARB: - score += 3; + ADJUST_SCORE(3); break; case HOLD_EFFECT_UTILITY_UMBRELLA: if (aiData->abilities[battlerAtk] != ABILITY_SOLAR_POWER && aiData->abilities[battlerAtk] != ABILITY_DRY_SKIN) @@ -4448,12 +4415,12 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score { case ABILITY_SWIFT_SWIM: if (AI_GetWeather(aiData) & B_WEATHER_RAIN) - score += 3; // Slow 'em down + ADJUST_SCORE(3); // Slow 'em down break; case ABILITY_CHLOROPHYLL: case ABILITY_FLOWER_GIFT: if (AI_GetWeather(aiData) & B_WEATHER_SUN) - score += 3; // Slow 'em down + ADJUST_SCORE(3); // Slow 'em down break; } } @@ -4462,7 +4429,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score //if (!IsRaidBattle() && IsDynamaxed(battlerDef) && gNewBS->dynamaxData.timer[battlerDef] > 1 && if (HasDamagingMove(battlerAtk) || (isDoubleBattle && IsBattlerAlive(BATTLE_PARTNER(battlerAtk)) && HasDamagingMove(BATTLE_PARTNER(battlerAtk)))) - score += 2; // Force 'em out next turn + ADJUST_SCORE(2); // Force 'em out next turn break; default: if (move != MOVE_BESTOW && aiData->items[battlerAtk] == ITEM_NONE) @@ -4473,25 +4440,25 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score break; case HOLD_EFFECT_TOXIC_ORB: if (ShouldPoisonSelf(battlerAtk, aiData->abilities[battlerAtk])) - score += 2; + ADJUST_SCORE(2); break; case HOLD_EFFECT_FLAME_ORB: if (ShouldBurnSelf(battlerAtk, aiData->abilities[battlerAtk])) - score += 2; + ADJUST_SCORE(2); break; case HOLD_EFFECT_BLACK_SLUDGE: if (IS_BATTLER_OF_TYPE(battlerAtk, TYPE_POISON) || aiData->abilities[battlerAtk] == ABILITY_MAGIC_GUARD) - score += 3; + ADJUST_SCORE(3); break; case HOLD_EFFECT_IRON_BALL: if (HasMoveEffect(battlerAtk, EFFECT_FLING)) - score += 2; + ADJUST_SCORE(2); break; case HOLD_EFFECT_LAGGING_TAIL: case HOLD_EFFECT_STICKY_BARB: break; default: - score++; //other hold effects generally universally good + ADJUST_SCORE(1); //other hold effects generally universally good break; } } @@ -4502,49 +4469,49 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score && !IsRolePlayBannedAbility(aiData->abilities[battlerDef]) && !IsAbilityOfRating(aiData->abilities[battlerAtk], 5) && IsAbilityOfRating(aiData->abilities[battlerDef], 5)) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_INGRAIN: if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_BIG_ROOT) - score += 3; + ADJUST_SCORE(3); else - score++; + ADJUST_SCORE(1); break; case EFFECT_SUPERPOWER: case EFFECT_OVERHEAT: case EFFECT_MAKE_IT_RAIN: if (aiData->abilities[battlerAtk] == ABILITY_CONTRARY) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_MAGIC_COAT: if (IS_MOVE_STATUS(predictedMove) && AI_GetBattlerMoveTargetType(battlerDef, predictedMove) & (MOVE_TARGET_SELECTED | MOVE_TARGET_OPPONENTS_FIELD | MOVE_TARGET_BOTH)) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_RECYCLE: if (GetUsedHeldItem(battlerAtk) != ITEM_NONE) - score++; + ADJUST_SCORE(1); if (IsRecycleEncouragedItem(GetUsedHeldItem(battlerAtk))) - score++; + ADJUST_SCORE(1); if (aiData->abilities[battlerAtk] == ABILITY_RIPEN) { u32 item = GetUsedHeldItem(battlerAtk); u32 toHeal = (ItemId_GetHoldEffectParam(item) == 10) ? 10 : gBattleMons[battlerAtk].maxHP / ItemId_GetHoldEffectParam(item); if (IsStatBoostingBerry(item) && aiData->hpPercents[battlerAtk] > 60) - score++; + ADJUST_SCORE(1); else if (ShouldRestoreHpBerry(battlerAtk, item) && !CanAIFaintTarget(battlerAtk, battlerDef, 0) && ((GetWhichBattlerFaster(battlerAtk, battlerDef, TRUE) == 0 && CanTargetFaintAiWithMod(battlerDef, battlerAtk, 0, 0)) || !CanTargetFaintAiWithMod(battlerDef, battlerAtk, toHeal, 0))) - score++; // Recycle healing berry if we can't otherwise faint the target and the target wont kill us after we activate the berry + ADJUST_SCORE(1); // Recycle healing berry if we can't otherwise faint the target and the target wont kill us after we activate the berry } break; case EFFECT_BRICK_BREAK: if (gSideStatuses[GetBattlerSide(battlerDef)] & SIDE_STATUS_REFLECT) - score++; + ADJUST_SCORE(1); if (gSideStatuses[GetBattlerSide(battlerDef)] & SIDE_STATUS_LIGHTSCREEN) - score++; + ADJUST_SCORE(1); if (gSideStatuses[GetBattlerSide(battlerDef)] & SIDE_STATUS_AURORA_VEIL) - score++; + ADJUST_SCORE(1); break; case EFFECT_KNOCK_OFF: if (CanKnockOffItem(battlerDef, aiData->items[battlerDef])) @@ -4553,49 +4520,49 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score { case HOLD_EFFECT_IRON_BALL: if (HasMoveEffect(battlerDef, EFFECT_FLING)) - score += 4; + ADJUST_SCORE(4); break; case HOLD_EFFECT_LAGGING_TAIL: case HOLD_EFFECT_STICKY_BARB: break; default: - score += 3; + ADJUST_SCORE(3); break; } } break; case EFFECT_SKILL_SWAP: if (GetAbilityRating(aiData->abilities[battlerDef]) > GetAbilityRating(aiData->abilities[battlerAtk])) - score++; + ADJUST_SCORE(1); break; case EFFECT_WORRY_SEED: case EFFECT_GASTRO_ACID: case EFFECT_SIMPLE_BEAM: if (IsAbilityOfRating(aiData->abilities[battlerDef], 5)) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_ENTRAINMENT: if (IsAbilityOfRating(aiData->abilities[battlerDef], 5) || GetAbilityRating(aiData->abilities[battlerAtk]) <= 0) { if (aiData->abilities[battlerDef] != aiData->abilities[battlerAtk] && !(gStatuses3[battlerDef] & STATUS3_GASTRO_ACID)) - score += 2; + ADJUST_SCORE(2); } break; case EFFECT_IMPRISON: if (predictedMove != MOVE_NONE && HasMove(battlerAtk, predictedMove)) - score += 3; + ADJUST_SCORE(3); else if (gDisableStructs[battlerAtk].isFirstTurn == 0) - score++; + ADJUST_SCORE(1); break; case EFFECT_REFRESH: if (gBattleMons[battlerAtk].status1 & STATUS1_ANY) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_TAKE_HEART: if (gBattleMons[battlerAtk].status1 & STATUS1_ANY || BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPATK) || BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPDEF)) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_PSYCHO_SHIFT: if (gBattleMons[battlerAtk].status1 & STATUS1_PSN_ANY) @@ -4613,25 +4580,25 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score break; case EFFECT_SNATCH: if (predictedMove != MOVE_NONE && gBattleMoves[predictedMove].snatchAffected) - score += 3; // Steal move + ADJUST_SCORE(3); // Steal move break; case EFFECT_MUD_SPORT: if (!HasMoveWithType(battlerAtk, TYPE_ELECTRIC) && HasMoveWithType(battlerDef, TYPE_ELECTRIC)) - score++; + ADJUST_SCORE(1); break; case EFFECT_WATER_SPORT: if (!HasMoveWithType(battlerAtk, TYPE_FIRE) && (HasMoveWithType(battlerDef, TYPE_FIRE))) - score++; + ADJUST_SCORE(1); break; case EFFECT_TICKLE: if (gBattleMons[battlerDef].statStages[STAT_DEF] > 4 && HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL) && aiData->abilities[battlerDef] != ABILITY_CONTRARY && ShouldLowerDefense(battlerAtk, battlerDef, aiData->abilities[battlerDef])) { - score += 2; + ADJUST_SCORE(2); } else if (ShouldLowerAttack(battlerAtk, battlerDef, aiData->abilities[battlerDef])) { - score += 2; + ADJUST_SCORE(2); } break; case EFFECT_COSMIC_POWER: @@ -4648,7 +4615,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score break; case EFFECT_GEOMANCY: if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_POWER_HERB) - score += 3; + ADJUST_SCORE(3); //fallthrough case EFFECT_QUIVER_DANCE: IncreaseStatUpScore(battlerAtk, battlerDef, STAT_SPEED, &score); @@ -4662,7 +4629,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score break; case EFFECT_SHELL_SMASH: if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_RESTORE_STATS) - score += 1; + ADJUST_SCORE(1); IncreaseStatUpScore(battlerAtk, battlerDef, STAT_SPEED, &score); IncreaseStatUpScore(battlerAtk, battlerDef, STAT_SPATK, &score); @@ -4676,24 +4643,24 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score case EFFECT_GUARD_SWAP: if (gBattleMons[battlerDef].statStages[STAT_DEF] > gBattleMons[battlerAtk].statStages[STAT_DEF] && gBattleMons[battlerDef].statStages[STAT_SPDEF] >= gBattleMons[battlerAtk].statStages[STAT_SPDEF]) - score++; + ADJUST_SCORE(1); else if (gBattleMons[battlerDef].statStages[STAT_SPDEF] > gBattleMons[battlerAtk].statStages[STAT_SPDEF] && gBattleMons[battlerDef].statStages[STAT_DEF] >= gBattleMons[battlerAtk].statStages[STAT_DEF]) - score++; + ADJUST_SCORE(1); break; case EFFECT_POWER_SWAP: if (gBattleMons[battlerDef].statStages[STAT_ATK] > gBattleMons[battlerAtk].statStages[STAT_ATK] && gBattleMons[battlerDef].statStages[STAT_SPATK] >= gBattleMons[battlerAtk].statStages[STAT_SPATK]) - score++; + ADJUST_SCORE(1); else if (gBattleMons[battlerDef].statStages[STAT_SPATK] > gBattleMons[battlerAtk].statStages[STAT_SPATK] && gBattleMons[battlerDef].statStages[STAT_ATK] >= gBattleMons[battlerAtk].statStages[STAT_ATK]) - score++; + ADJUST_SCORE(1); break; case EFFECT_POWER_TRICK: if (!(gStatuses3[battlerAtk] & STATUS3_POWER_TRICK)) { if (gBattleMons[battlerAtk].defense > gBattleMons[battlerAtk].attack && HasMoveWithSplit(battlerAtk, SPLIT_PHYSICAL)) - score += 2; + ADJUST_SCORE(2); break; } break; @@ -4709,13 +4676,13 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score hasHigherStat = TRUE; } if (hasHigherStat && i == NUM_BATTLE_STATS) - score++; + ADJUST_SCORE(1); } break; case EFFECT_SPEED_SWAP: // TODO this is cheating a bit... if (gBattleMons[battlerDef].speed > gBattleMons[battlerAtk].speed) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_GUARD_SPLIT: { @@ -4725,7 +4692,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score if ((newDefense > gBattleMons[battlerAtk].defense && newSpDef >= gBattleMons[battlerAtk].spDefense) || (newSpDef > gBattleMons[battlerAtk].spDefense && newDefense >= gBattleMons[battlerAtk].defense)) - score++; + ADJUST_SCORE(1); } break; case EFFECT_POWER_SPLIT: @@ -4735,69 +4702,69 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score if ((newAttack > gBattleMons[battlerAtk].attack && newSpAtk >= gBattleMons[battlerAtk].spAttack) || (newSpAtk > gBattleMons[battlerAtk].spAttack && newAttack >= gBattleMons[battlerAtk].attack)) - score++; + ADJUST_SCORE(1); } break; case EFFECT_BUG_BITE: // And pluck if (gBattleMons[battlerDef].status2 & STATUS2_SUBSTITUTE || aiData->abilities[battlerDef] == ABILITY_STICKY_HOLD) break; else if (ItemId_GetPocket(aiData->items[battlerDef]) == POCKET_BERRIES) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_INCINERATE: if (gBattleMons[battlerDef].status2 & STATUS2_SUBSTITUTE || aiData->abilities[battlerDef] == ABILITY_STICKY_HOLD) break; else if (ItemId_GetPocket(aiData->items[battlerDef]) == POCKET_BERRIES || aiData->holdEffects[battlerDef] == HOLD_EFFECT_GEMS) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_SMACK_DOWN: if (!IsBattlerGrounded(battlerDef)) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_RELIC_SONG: if (!(gBattleMons[battlerAtk].status2 & STATUS2_TRANSFORMED)) // Don't try to change form if it's transformed. { if (gBattleMons[battlerAtk].species == SPECIES_MELOETTA && gBattleMons[battlerDef].defense < gBattleMons[battlerDef].spDefense) - score += 3; // Change to Pirouette if can do more damage + ADJUST_SCORE(3); // Change to Pirouette if can do more damage else if (gBattleMons[battlerAtk].species == SPECIES_MELOETTA_PIROUETTE && gBattleMons[battlerDef].spDefense < gBattleMons[battlerDef].defense) - score += 3; // Change to Aria if can do more damage + ADJUST_SCORE(3); // Change to Aria if can do more damage } break; case EFFECT_ELECTRIC_TERRAIN: case EFFECT_MISTY_TERRAIN: if (gStatuses3[battlerAtk] & STATUS3_YAWN && IsBattlerGrounded(battlerAtk)) - score += 10; + ADJUST_SCORE(10); //fallthrough case EFFECT_GRASSY_TERRAIN: case EFFECT_PSYCHIC_TERRAIN: - score += 2; + ADJUST_SCORE(2); if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_TERRAIN_EXTENDER) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_PLEDGE: if (isDoubleBattle) { if (HasMoveEffect(BATTLE_PARTNER(battlerAtk), EFFECT_PLEDGE)) - score += 3; // Partner might use pledge move + ADJUST_SCORE(3); // Partner might use pledge move } break; case EFFECT_TRICK_ROOM: if (!(gFieldStatuses & STATUS_FIELD_TRICK_ROOM) && GetBattlerSideSpeedAverage(battlerAtk) < GetBattlerSideSpeedAverage(battlerDef)) - score += 3; + ADJUST_SCORE(3); else if ((gFieldStatuses & STATUS_FIELD_TRICK_ROOM) && GetBattlerSideSpeedAverage(battlerAtk) >= GetBattlerSideSpeedAverage(battlerDef)) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_MAGIC_ROOM: - score++; + ADJUST_SCORE(1); if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_NONE && aiData->holdEffects[battlerDef] != HOLD_EFFECT_NONE) - score++; + ADJUST_SCORE(1); if (isDoubleBattle && aiData->holdEffects[BATTLE_PARTNER(battlerAtk)] == HOLD_EFFECT_NONE && aiData->holdEffects[BATTLE_PARTNER(battlerDef)] != HOLD_EFFECT_NONE) - score++; + ADJUST_SCORE(1); break; case EFFECT_WONDER_ROOM: if ((HasMoveWithSplit(battlerDef, SPLIT_PHYSICAL) && gBattleMons[battlerAtk].defense < gBattleMons[battlerAtk].spDefense) || (HasMoveWithSplit(battlerDef, SPLIT_SPECIAL) && gBattleMons[battlerAtk].spDefense < gBattleMons[battlerAtk].defense)) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_GRAVITY: if (!(gFieldStatuses & STATUS_FIELD_GRAVITY)) @@ -4805,9 +4772,9 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score if (HasSleepMoveWithLowAccuracy(battlerAtk, battlerDef)) // Has Gravity for a move like Hypnosis IncreaseSleepScore(battlerAtk, battlerDef, move, &score); else if (HasMoveWithLowAccuracy(battlerAtk, battlerDef, 90, FALSE, aiData->abilities[battlerAtk], aiData->abilities[battlerDef], aiData->holdEffects[battlerAtk], aiData->holdEffects[battlerDef])) - score += 2; + ADJUST_SCORE(2); else - score++; + ADJUST_SCORE(1); } break; case EFFECT_ION_DELUGE: @@ -4815,7 +4782,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score || aiData->abilities[battlerAtk] == ABILITY_MOTOR_DRIVE || aiData->abilities[battlerAtk] == ABILITY_LIGHTNING_ROD) && gBattleMoves[predictedMove].type == TYPE_NORMAL) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_FLING: /* TODO @@ -4836,47 +4803,47 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score break; case MOVE_EFFECT_FREEZE: if (AI_CanFreeze(battlerAtk, battlerDef)) - score += 3; + ADJUST_SCORE(3); break; }*/ break; case EFFECT_FEINT: if (gBattleMoves[predictedMove].effect == EFFECT_PROTECT) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_EMBARGO: if (aiData->holdEffects[battlerDef] != HOLD_EFFECT_NONE) - score++; + ADJUST_SCORE(1); break; case EFFECT_POWDER: if (predictedMove != MOVE_NONE && !IS_MOVE_STATUS(predictedMove) && gBattleMoves[predictedMove].type == TYPE_FIRE) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_TELEKINESIS: if (HasMoveWithLowAccuracy(battlerAtk, battlerDef, 90, FALSE, aiData->abilities[battlerAtk], aiData->abilities[battlerDef], aiData->holdEffects[battlerAtk], aiData->holdEffects[battlerDef]) || !IsBattlerGrounded(battlerDef)) - score++; + ADJUST_SCORE(1); break; case EFFECT_THROAT_CHOP: if (predictedMove != MOVE_NONE && gBattleMoves[predictedMove].soundMove && AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER) - score += 3; // Ai goes first and predicts the target will use a sound move + ADJUST_SCORE(3); // Ai goes first and predicts the target will use a sound move else if (HasSoundMove(battlerDef)) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_HEAL_BLOCK: if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER && predictedMove != MOVE_NONE && IsHealingMoveEffect(gBattleMoves[predictedMove].effect)) - score += 3; // Try to cancel healing move + ADJUST_SCORE(3); // Try to cancel healing move else if (HasHealingEffect(battlerDef) || aiData->holdEffects[battlerDef] == HOLD_EFFECT_LEFTOVERS || (aiData->holdEffects[battlerDef] == HOLD_EFFECT_BLACK_SLUDGE && IS_BATTLER_OF_TYPE(battlerDef, TYPE_POISON))) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_SOAK: if (HasMoveWithType(battlerAtk, TYPE_ELECTRIC) || HasMoveWithType(battlerAtk, TYPE_GRASS) || HasMoveEffect(battlerAtk, EFFECT_FREEZE_DRY)) - score += 2; // Get some super effective moves + ADJUST_SCORE(2); // Get some super effective moves break; case EFFECT_THIRD_TYPE: if (aiData->abilities[battlerDef] == ABILITY_WONDER_GUARD) - score += 2; // Give target more weaknesses + ADJUST_SCORE(2); // Give target more weaknesses break; case EFFECT_ELECTRIFY: if (predictedMove != MOVE_NONE @@ -4884,38 +4851,38 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score || aiData->abilities[battlerAtk] == ABILITY_MOTOR_DRIVE || aiData->abilities[battlerAtk] == ABILITY_LIGHTNING_ROD)) { - score += 3; + ADJUST_SCORE(3); } break; case EFFECT_TOPSY_TURVY: if (CountPositiveStatStages(battlerDef) > CountNegativeStatStages(battlerDef)) - score++; + ADJUST_SCORE(1); break; case EFFECT_FAIRY_LOCK: if (!IsBattlerTrapped(battlerDef, TRUE)) { if (ShouldTrap(battlerAtk, battlerDef, move)) - score += 8; + ADJUST_SCORE(8); } break; case EFFECT_QUASH: if (isDoubleBattle && AI_WhoStrikesFirst(BATTLE_PARTNER(battlerAtk), battlerDef, aiData->partnerMove) == AI_IS_SLOWER) // Attacker partner wouldn't go before target - score++; + ADJUST_SCORE(1); break; case EFFECT_TAILWIND: if (GetBattlerSideSpeedAverage(battlerAtk) < GetBattlerSideSpeedAverage(battlerDef)) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_LUCKY_CHANT: if (!isDoubleBattle) { - score++; + ADJUST_SCORE(1); } else { if (CountUsablePartyMons(battlerDef) > 0) - score += 8; + ADJUST_SCORE(8); } break; case EFFECT_MAGNET_RISE: @@ -4925,13 +4892,13 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER) // Attacker goes first { if (gBattleMoves[predictedMove].type == TYPE_GROUND) - score += 3; // Cause the enemy's move to fail + ADJUST_SCORE(3); // Cause the enemy's move to fail break; } else // Opponent Goes First { if (HasDamagingMoveOfType(battlerDef, TYPE_GROUND)) - score += 2; + ADJUST_SCORE(2); break; } } @@ -4939,7 +4906,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score case EFFECT_CAMOUFLAGE: if (predictedMove != MOVE_NONE && AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER // Attacker goes first && !IS_MOVE_STATUS(move) && AI_GetTypeEffectiveness(predictedMove, battlerDef, battlerAtk) != AI_EFFECTIVENESS_x0) - score++; + ADJUST_SCORE(1); break; case EFFECT_FLAME_BURST: if (isDoubleBattle) @@ -4948,7 +4915,7 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score && aiData->hpPercents[BATTLE_PARTNER(battlerDef)] < 12 && aiData->abilities[BATTLE_PARTNER(battlerDef)] != ABILITY_MAGIC_GUARD && !IS_BATTLER_OF_TYPE(BATTLE_PARTNER(battlerDef), TYPE_FIRE)) - score++; + ADJUST_SCORE(1); } break; case EFFECT_TOXIC_THREAD: @@ -4958,86 +4925,86 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score case EFFECT_SOLAR_BEAM: if (GetNoOfHitsToKOBattler(battlerAtk, battlerDef, movesetIndex) >= 2 && HasMoveEffect(battlerAtk, EFFECT_SUNNY_DAY) && (AI_GetWeather(aiData) & B_WEATHER_SUN)) // Use Sunny Day to boost damage. - score -= 3; + ADJUST_SCORE(-3); case EFFECT_TWO_TURNS_ATTACK: case EFFECT_SKULL_BASH: if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_POWER_HERB) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_COUNTER: if (!IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) && predictedMove != MOVE_NONE) { if (gDisableStructs[battlerDef].tauntTimer != 0) - score++; // target must use damaging move + ADJUST_SCORE(1); // target must use damaging move if (GetMoveDamageResult(battlerDef, battlerAtk, predictedMoveSlot) >= MOVE_POWER_GOOD && GetBattleMoveSplit(predictedMove) == SPLIT_PHYSICAL) - score += 3; + ADJUST_SCORE(3); } break; case EFFECT_MIRROR_COAT: if (!IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) && predictedMove != MOVE_NONE) { if (gDisableStructs[battlerDef].tauntTimer != 0) - score++; // target must use damaging move + ADJUST_SCORE(1); // target must use damaging move if (GetMoveDamageResult(battlerDef, battlerAtk, predictedMoveSlot) >= MOVE_POWER_GOOD && GetBattleMoveSplit(predictedMove) == SPLIT_SPECIAL) - score += 3; + ADJUST_SCORE(3); } break; case EFFECT_FLAIL: if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER) // Ai goes first { if (aiData->hpPercents[battlerAtk] < 20) - score++; + ADJUST_SCORE(1); else if (aiData->hpPercents[battlerAtk] < 8) - score += 2; + ADJUST_SCORE(2); } break; case EFFECT_SHORE_UP: if ((AI_GetWeather(aiData) & B_WEATHER_SANDSTORM) && ShouldRecover(battlerAtk, battlerDef, move, 67)) - score += 3; + ADJUST_SCORE(3); else if (ShouldRecover(battlerAtk, battlerDef, move, 50)) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_FACADE: if (gBattleMons[battlerAtk].status1 & (STATUS1_POISON | STATUS1_BURN | STATUS1_PARALYSIS | STATUS1_TOXIC_POISON | STATUS1_FROSTBITE)) - score++; + ADJUST_SCORE(1); break; case EFFECT_FOCUS_PUNCH: if (!isDoubleBattle && effectiveness > AI_EFFECTIVENESS_x0_5) { if (IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef])) - score += 2; + ADJUST_SCORE(2); else if (gBattleMons[battlerDef].status2 & (STATUS2_INFATUATION | STATUS2_CONFUSION)) - score++; + ADJUST_SCORE(1); } break; case EFFECT_SMELLINGSALT: if (gBattleMons[battlerDef].status1 & STATUS1_PARALYSIS) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_WAKE_UP_SLAP: if (gBattleMons[battlerDef].status1 & STATUS1_SLEEP) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_REVENGE: if (!(gBattleMons[battlerDef].status1 & STATUS1_SLEEP) && !(gBattleMons[battlerDef].status2 & (STATUS2_INFATUATION | STATUS2_CONFUSION))) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_ENDEAVOR: if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_SLOWER) // Opponent faster { if (aiData->hpPercents[battlerAtk] < 40) - score++; + ADJUST_SCORE(1); } else if (aiData->hpPercents[battlerAtk] < 50) { - score++; + ADJUST_SCORE(1); } break; case EFFECT_REVIVAL_BLESSING: if (GetFirstFaintedPartyIndex(battlerAtk) != PARTY_SIZE) - score += 2; + ADJUST_SCORE(2); break; //case EFFECT_EXTREME_EVOBOOST: // TODO //break; @@ -5052,11 +5019,11 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score || ShouldRecover(BATTLE_PARTNER(battlerAtk), battlerDef, move, 25) || gBattleMons[battlerAtk].status1 & STATUS1_ANY || gBattleMons[BATTLE_PARTNER(battlerAtk)].status1 & STATUS1_ANY) - score += 3; + ADJUST_SCORE(3); break; case EFFECT_SALT_CURE: if (IS_BATTLER_OF_TYPE(battlerDef, TYPE_WATER) || IS_BATTLER_OF_TYPE(battlerDef, TYPE_STEEL)) - score += 2; + ADJUST_SCORE(2); break; } // move effect checks @@ -5165,7 +5132,7 @@ static s32 AI_SetupFirstTurn(u32 battlerAtk, u32 battlerDef, u32 move, s32 score case EFFECT_GEOMANCY: case EFFECT_VICTORY_DANCE: case EFFECT_HIT_SET_ENTRY_HAZARD: - score += 2; + ADJUST_SCORE(2); break; default: break; @@ -5181,7 +5148,7 @@ static s32 AI_Risky(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) return score; if (gBattleMoves[move].highCritRatio) - score += 2; + ADJUST_SCORE(2); switch (gBattleMoves[move].effect) { @@ -5204,7 +5171,7 @@ static s32 AI_Risky(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_REVENGE: case EFFECT_TEETER_DANCE: if (Random() & 1) - score += 2; + ADJUST_SCORE(2); break; default: break; @@ -5220,7 +5187,7 @@ static s32 AI_PreferStrongestMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 return score; if (GetMoveDamageResult(battlerAtk, battlerDef, AI_THINKING_STRUCT->movesetIndex) == MOVE_POWER_BEST) - score += 2; + ADJUST_SCORE(2); return score; } @@ -5240,11 +5207,11 @@ static s32 AI_PreferBatonPass(u32 battlerAtk, u32 battlerDef, u32 move, s32 scor if (IsStatRaisingEffect(gBattleMoves[move].effect)) { if (gBattleResults.battleTurnCounter == 0) - score += 5; + ADJUST_SCORE(5); else if (AI_DATA->hpPercents[battlerAtk] < 60) - score -= 10; + ADJUST_SCORE(-10); else - score++; + ADJUST_SCORE(1); } // other specific checks @@ -5252,17 +5219,17 @@ static s32 AI_PreferBatonPass(u32 battlerAtk, u32 battlerDef, u32 move, s32 scor { case EFFECT_INGRAIN: if (!(gStatuses3[battlerAtk] & STATUS3_ROOTED)) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_AQUA_RING: if (!(gStatuses3[battlerAtk] & STATUS3_AQUA_RING)) - score += 2; + ADJUST_SCORE(2); break; case EFFECT_PROTECT: if (gLastMoves[battlerAtk] == MOVE_PROTECT || gLastMoves[battlerAtk] == MOVE_DETECT) - score -= 2; + ADJUST_SCORE(-2); else - score += 2; + ADJUST_SCORE(2); break; case EFFECT_BATON_PASS: for (i = STAT_ATK; i < NUM_BATTLE_STATS; i++) @@ -5270,9 +5237,9 @@ static s32 AI_PreferBatonPass(u32 battlerAtk, u32 battlerDef, u32 move, s32 scor IncreaseStatUpScore(battlerAtk, battlerDef, i, &score); } if (gStatuses3[battlerAtk] & (STATUS3_ROOTED | STATUS3_AQUA_RING)) - score += 2; + ADJUST_SCORE(2); if (gStatuses3[battlerAtk] & STATUS3_LEECHSEED) - score -= 3; + ADJUST_SCORE(-3); break; default: break; @@ -5300,10 +5267,10 @@ static s32 AI_HPAware(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (CanTargetFaintAi(FOE(battlerAtk), BATTLE_PARTNER(battlerAtk)) || (CanTargetFaintAi(BATTLE_PARTNER(FOE(battlerAtk)), BATTLE_PARTNER(battlerAtk)))) - score--; + ADJUST_SCORE(-1); if (AI_DATA->hpPercents[battlerDef] <= 50) - score++; + ADJUST_SCORE(1); } } else @@ -5328,7 +5295,7 @@ static s32 AI_HPAware(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_ROOST: case EFFECT_MEMENTO: case EFFECT_GRUDGE: - score -= 2; + ADJUST_SCORE(-2); break; default: break; @@ -5338,7 +5305,7 @@ static s32 AI_HPAware(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { // med hp if (IsStatRaisingEffect(effect) || IsStatLoweringEffect(effect)) - score -= 2; + ADJUST_SCORE(-2); switch (effect) { @@ -5351,7 +5318,7 @@ static s32 AI_HPAware(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_CONVERSION_2: case EFFECT_SAFEGUARD: case EFFECT_BELLY_DRUM: - score -= 2; + ADJUST_SCORE(-2); break; default: break; @@ -5361,7 +5328,7 @@ static s32 AI_HPAware(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { // low hp if (IsStatRaisingEffect(effect) || IsStatLoweringEffect(effect)) - score -= 2; + ADJUST_SCORE(-2); // check other discouraged low hp effects switch (effect) @@ -5389,7 +5356,7 @@ static s32 AI_HPAware(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_HAIL: case EFFECT_SNOWSCAPE: case EFFECT_RAIN_DANCE: - score -= 2; + ADJUST_SCORE(-2); break; default: break; @@ -5400,7 +5367,7 @@ static s32 AI_HPAware(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) // consider target HP if (CanIndexMoveFaintTarget(battlerAtk, battlerDef, AI_THINKING_STRUCT->movesetIndex, 0)) { - score += 2; + ADJUST_SCORE(2); } else { @@ -5456,7 +5423,7 @@ static s32 AI_HPAware(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_DRAGON_DANCE: case EFFECT_DEFENSE_UP_3: case EFFECT_SPECIAL_ATTACK_UP_3: - score -= 2; + ADJUST_SCORE(-2); break; default: break; @@ -5466,7 +5433,7 @@ static s32 AI_HPAware(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { // low HP if (IS_MOVE_STATUS(move)) - score -= 2; // don't use status moves if target is at low health + ADJUST_SCORE(-2); // don't use status moves if target is at low health } } diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 55ff27fbfbf0..b6d3d4f363a4 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -744,6 +744,13 @@ s32 AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 battlerDef, u8 *type AI_CalcDamage(move, battlerAtk, battlerDef, typeEffectiveness, considerZPower, AI_GetWeather(AI_DATA)); } +static inline s32 LowestRollDmg(s32 dmg) +{ + dmg *= 100 - 15; + dmg /= 100; + return dmg; +} + s32 AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower, u32 weather) { s32 dmg, moveType; @@ -802,11 +809,11 @@ s32 AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectivenes aiData->abilities[battlerAtk], aiData->abilities[battlerDef]); u32 critChance = GetCritHitChance(critChanceIndex); // With critChance getting closer to 1, dmg gets closer to critDmg. - dmg = (critDmg + normalDmg * (critChance - 1)) / (critChance); + dmg = LowestRollDmg((critDmg + normalDmg * (critChance - 1)) / (critChance)); } else { - dmg = normalDmg; + dmg = LowestRollDmg(normalDmg); } if (!gBattleStruct->zmove.active) @@ -890,7 +897,7 @@ bool32 AI_IsDamagedByRecoil(u32 battler) } // Decide whether move having an additional effect for . -static bool32 AI_IsMoveEffectInPlus(u32 battlerAtk, u32 battlerDef, u32 move) +static bool32 AI_IsMoveEffectInPlus(u32 battlerAtk, u32 battlerDef, u32 move, s32 noOfHitsToKo) { u32 i; u32 abilityDef = AI_DATA->abilities[battlerDef]; @@ -966,32 +973,32 @@ static bool32 AI_IsMoveEffectInPlus(u32 battlerAtk, u32 battlerDef, u32 move) return TRUE; break; case EFFECT_ATTACK_DOWN_HIT: - if (ShouldLowerStat(battlerDef, abilityDef, STAT_ATK) && abilityDef != ABILITY_HYPER_CUTTER) + if (ShouldLowerStat(battlerDef, abilityDef, STAT_ATK) && abilityDef != ABILITY_HYPER_CUTTER && noOfHitsToKo != 1) return TRUE; break; case EFFECT_DEFENSE_DOWN_HIT: - if (ShouldLowerStat(battlerDef, abilityDef, STAT_DEF)) + if (ShouldLowerStat(battlerDef, abilityDef, STAT_DEF) && noOfHitsToKo != 1) return TRUE; break; case EFFECT_SPEED_DOWN_HIT: - if (ShouldLowerStat(battlerDef, abilityDef, STAT_SPEED)) + if (ShouldLowerStat(battlerDef, abilityDef, STAT_SPEED) && noOfHitsToKo != 1) return TRUE; break; case EFFECT_SPECIAL_ATTACK_DOWN_HIT: - if (ShouldLowerStat(battlerDef, abilityDef, STAT_SPATK)) + if (ShouldLowerStat(battlerDef, abilityDef, STAT_SPATK) && noOfHitsToKo != 1) return TRUE; break; case EFFECT_SPECIAL_DEFENSE_DOWN_HIT: case EFFECT_SPECIAL_DEFENSE_DOWN_HIT_2: - if (ShouldLowerStat(battlerDef, abilityDef, STAT_SPDEF)) + if (ShouldLowerStat(battlerDef, abilityDef, STAT_SPDEF) && noOfHitsToKo != 1) return TRUE; break; case EFFECT_ACCURACY_DOWN_HIT: - if (ShouldLowerStat(battlerDef, abilityDef, STAT_ACC)) + if (ShouldLowerStat(battlerDef, abilityDef, STAT_ACC) && noOfHitsToKo != 1) return TRUE; break; case EFFECT_EVASION_DOWN_HIT: - if (ShouldLowerStat(battlerDef, abilityDef, STAT_EVASION)) + if (ShouldLowerStat(battlerDef, abilityDef, STAT_EVASION) && noOfHitsToKo != 1) return TRUE; break; case EFFECT_ALL_STATS_UP_HIT: @@ -1006,8 +1013,38 @@ static bool32 AI_IsMoveEffectInPlus(u32 battlerAtk, u32 battlerDef, u32 move) return FALSE; } +static bool32 AI_IsMoveEffectInMinus(u32 battlerAtk, u32 battlerDef, u32 move, s32 noOfHitsToKo) +{ + u32 abilityAtk = AI_DATA->abilities[battlerAtk]; + u32 abilityDef = AI_DATA->abilities[battlerDef]; + + switch (gBattleMoves[move].effect) + { + case EFFECT_RECHARGE: + return TRUE; + case EFFECT_RECOIL_25: + case EFFECT_RECOIL_IF_MISS: + case EFFECT_RECOIL_50: + case EFFECT_RECOIL_33: + case EFFECT_RECOIL_33_STATUS: + if (AI_IsDamagedByRecoil(battlerAtk)) + return TRUE; + break; + case EFFECT_SPEED_DOWN_HIT: + case EFFECT_ATTACK_DOWN_HIT: + case EFFECT_DEFENSE_DOWN_HIT: + case EFFECT_SPECIAL_ATTACK_DOWN_HIT: + case EFFECT_SPECIAL_DEFENSE_DOWN_HIT: + case EFFECT_SPECIAL_DEFENSE_DOWN_HIT_2: + if (noOfHitsToKo != 1 && abilityDef == ABILITY_CONTRARY && !IsMoldBreakerTypeAbility(abilityAtk)) + return TRUE; + break; + } + return FALSE; +} + // Checks if one of the moves has side effects or perks, assuming equal dmg or equal no of hits to KO -u32 AI_WhichMoveBetter(u32 move1, u32 move2, u32 battlerAtk, u32 battlerDef) +u32 AI_WhichMoveBetter(u32 move1, u32 move2, u32 battlerAtk, u32 battlerDef, s32 noOfHitsToKo) { bool32 effect1, effect2; s32 defAbility = AI_DATA->abilities[battlerDef]; @@ -1022,22 +1059,18 @@ u32 AI_WhichMoveBetter(u32 move1, u32 move2, u32 battlerAtk, u32 battlerDef) if (IS_MOVE_PHYSICAL(move2) && !IS_MOVE_PHYSICAL(move1)) return 0; } - // Check recoil - if (AI_IsDamagedByRecoil(battlerAtk)) - { - if (IS_MOVE_RECOIL(move1) && !IS_MOVE_RECOIL(move2) && gBattleMoves[move2].effect != EFFECT_RECHARGE) - return 1; - if (IS_MOVE_RECOIL(move2) && !IS_MOVE_RECOIL(move1) && gBattleMoves[move1].effect != EFFECT_RECHARGE) - return 0; - } - // Check recharge - if (gBattleMoves[move1].effect == EFFECT_RECHARGE && gBattleMoves[move2].effect != EFFECT_RECHARGE) - return 1; - if (gBattleMoves[move2].effect == EFFECT_RECHARGE && gBattleMoves[move1].effect != EFFECT_RECHARGE) + + + // Check additional effects. + effect1 = AI_IsMoveEffectInMinus(battlerAtk, battlerDef, move1, noOfHitsToKo); + effect2 = AI_IsMoveEffectInMinus(battlerAtk, battlerDef, move2, noOfHitsToKo); + if (effect2 && !effect1) return 0; - // Check additional effect. - effect1 = AI_IsMoveEffectInPlus(battlerAtk, battlerDef, move1); - effect2 = AI_IsMoveEffectInPlus(battlerAtk, battlerDef, move2); + if (effect1 && !effect2) + return 1; + + effect1 = AI_IsMoveEffectInPlus(battlerAtk, battlerDef, move1, noOfHitsToKo); + effect2 = AI_IsMoveEffectInPlus(battlerAtk, battlerDef, move2, noOfHitsToKo); if (effect2 && !effect1) return 1; if (effect1 && !effect2) @@ -1132,7 +1165,7 @@ void SetMovesDamageResults(u32 battlerAtk, u16 *moves) bestId = j; if (moveDmgs[j] == moveDmgs[bestId]) { - switch (AI_WhichMoveBetter(gBattleMons[battlerAtk].moves[bestId], gBattleMons[battlerAtk].moves[j], battlerAtk, battlerDef)) + switch (AI_WhichMoveBetter(gBattleMons[battlerAtk].moves[bestId], gBattleMons[battlerAtk].moves[j], battlerAtk, battlerDef, GetNoOfHitsToKO(moveDmgs[j], hp))) { case 2: if (Random() & 1) @@ -1149,7 +1182,7 @@ void SetMovesDamageResults(u32 battlerAtk, u16 *moves) result = MOVE_POWER_BEST; else if ((moveDmgs[currId] >= hp || moveDmgs[bestId] < hp) // If current move can faint as well, or if neither can && GetNoOfHitsToKO(moveDmgs[currId], hp) - GetNoOfHitsToKO(moveDmgs[bestId], hp) <= 2 // Consider a move weak if it needs to be used at least 2 times more to faint the target, compared to the best move. - && AI_WhichMoveBetter(gBattleMons[battlerAtk].moves[bestId], gBattleMons[battlerAtk].moves[currId], battlerAtk, battlerDef) != 0) + && AI_WhichMoveBetter(gBattleMons[battlerAtk].moves[bestId], gBattleMons[battlerAtk].moves[currId], battlerAtk, battlerDef, GetNoOfHitsToKO(moveDmgs[currId], hp)) != 0) result = MOVE_POWER_GOOD; else result = MOVE_POWER_WEAK; @@ -1478,9 +1511,7 @@ bool32 DoesBattlerIgnoreAbilityChecks(u32 atkAbility, u32 move) return TRUE; } - if (atkAbility == ABILITY_MOLD_BREAKER - || atkAbility == ABILITY_TERAVOLT - || atkAbility == ABILITY_TURBOBLAZE) + if (IsMoldBreakerTypeAbility(atkAbility)) return TRUE; return FALSE; @@ -1612,12 +1643,6 @@ bool32 IsMoveRedirectionPrevented(u32 move, u32 atkAbility) return FALSE; } -u32 AI_GetMoveAccuracy(u32 battlerAtk, u32 battlerDef, u32 move) -{ - return GetTotalAccuracy(battlerAtk, battlerDef, move, AI_DATA->abilities[battlerAtk], AI_DATA->abilities[battlerDef], - AI_DATA->holdEffects[battlerAtk], AI_DATA->holdEffects[battlerDef]); -} - bool32 IsSemiInvulnerable(u32 battlerDef, u32 move) { if (gStatuses3[battlerDef] & STATUS3_PHANTOM_FORCE) @@ -1676,7 +1701,7 @@ bool32 IsMoveEncouragedToHit(u32 battlerAtk, u32 battlerDef, u32 move) bool32 ShouldTryOHKO(u32 battlerAtk, u32 battlerDef, u32 atkAbility, u32 defAbility, u32 move) { u32 holdEffect = AI_DATA->holdEffects[battlerDef]; - u32 accuracy = AI_GetMoveAccuracy(battlerAtk, battlerDef, move); + u32 accuracy = AI_DATA->moveAccuracy[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex]; gPotentialItemEffectBattler = battlerDef; if (holdEffect == HOLD_EFFECT_FOCUS_BAND && (Random() % 100) < AI_DATA->holdEffectParams[battlerDef]) @@ -1865,8 +1890,7 @@ void ProtectChecks(u32 battlerAtk, u32 battlerDef, u32 move, u32 predictedMove, // stat stages bool32 ShouldLowerStat(u32 battler, u32 battlerAbility, u32 stat) { - if ((gBattleMons[battler].statStages[stat] > MIN_STAT_STAGE && battlerAbility != ABILITY_CONTRARY) - || (battlerAbility == ABILITY_CONTRARY && gBattleMons[battler].statStages[stat] < MAX_STAT_STAGE)) + if (gBattleMons[battler].statStages[stat] > MIN_STAT_STAGE && battlerAbility != ABILITY_CONTRARY) { if (AI_DATA->holdEffects[battler] == HOLD_EFFECT_CLEAR_AMULET || battlerAbility == ABILITY_CLEAR_BODY @@ -1874,6 +1898,14 @@ bool32 ShouldLowerStat(u32 battler, u32 battlerAbility, u32 stat) || battlerAbility == ABILITY_FULL_METAL_BODY) return FALSE; + // If AI is faster and doesn't have any mons left, lowering speed doesn't give any + if (stat == STAT_SPEED) + { + if (AI_WhoStrikesFirst(sBattler_AI, battler, AI_THINKING_STRUCT->moveConsidered) == AI_IS_FASTER + && CountUsablePartyMons(sBattler_AI) == 0 + && !HasMoveEffect(sBattler_AI, EFFECT_ELECTRO_BALL)) + return FALSE; + } return TRUE; } @@ -2157,7 +2189,7 @@ bool32 HasMoveWithLowAccuracy(u32 battlerAtk, u32 battlerDef, u32 accCheck, bool || AI_GetBattlerMoveTargetType(battlerAtk, moves[i]) & (MOVE_TARGET_USER | MOVE_TARGET_OPPONENTS_FIELD)) continue; - if (AI_GetMoveAccuracy(battlerAtk, battlerDef, moves[i]) <= accCheck) + if (AI_DATA->moveAccuracy[battlerAtk][battlerDef][i] <= accCheck) return TRUE; } } @@ -2178,7 +2210,7 @@ bool32 HasSleepMoveWithLowAccuracy(u32 battlerAtk, u32 battlerDef) if (!(gBitTable[i] & moveLimitations)) { if (gBattleMoves[moves[i]].effect == EFFECT_SLEEP - && AI_GetMoveAccuracy(battlerAtk, battlerDef, moves[i]) < 85) + && AI_DATA->moveAccuracy[battlerAtk][battlerDef][i] < 85) return TRUE; } } @@ -3341,7 +3373,7 @@ bool32 DoesPartnerHaveSameMoveEffect(u32 battlerAtkPartner, u32 battlerDef, u32 return FALSE; if (gBattleMoves[move].effect == gBattleMoves[partnerMove].effect - && gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE + && partnerMove != MOVE_NONE && gBattleStruct->moveTarget[battlerAtkPartner] == battlerDef) { return TRUE; @@ -3356,7 +3388,7 @@ bool32 PartnerHasSameMoveEffectWithoutTarget(u32 battlerAtkPartner, u32 move, u3 return FALSE; if (gBattleMoves[move].effect == gBattleMoves[partnerMove].effect - && gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE) + && partnerMove != MOVE_NONE) return TRUE; return FALSE; } @@ -3367,7 +3399,7 @@ bool32 PartnerMoveEffectIsStatusSameTarget(u32 battlerAtkPartner, u32 battlerDef if (!IsDoubleBattle()) return FALSE; - if (gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE + if (partnerMove != MOVE_NONE && gBattleStruct->moveTarget[battlerAtkPartner] == battlerDef && (gBattleMoves[partnerMove].effect == EFFECT_SLEEP || gBattleMoves[partnerMove].effect == EFFECT_POISON @@ -3379,20 +3411,15 @@ bool32 PartnerMoveEffectIsStatusSameTarget(u32 battlerAtkPartner, u32 battlerDef return FALSE; } -//PARTNER_MOVE_EFFECT_IS_WEATHER -bool32 PartnerMoveEffectIsWeather(u32 battlerAtkPartner, u32 partnerMove) +bool32 IsMoveEffectWeather(u32 move) { - if (!IsDoubleBattle()) - return FALSE; - - if (gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE - && (gBattleMoves[partnerMove].effect == EFFECT_SUNNY_DAY - || gBattleMoves[partnerMove].effect == EFFECT_RAIN_DANCE - || gBattleMoves[partnerMove].effect == EFFECT_SANDSTORM - || gBattleMoves[partnerMove].effect == EFFECT_HAIL - || gBattleMoves[partnerMove].effect == EFFECT_SNOWSCAPE)) + if (move != MOVE_NONE + && (gBattleMoves[move].effect == EFFECT_SUNNY_DAY + || gBattleMoves[move].effect == EFFECT_RAIN_DANCE + || gBattleMoves[move].effect == EFFECT_SANDSTORM + || gBattleMoves[move].effect == EFFECT_HAIL + || gBattleMoves[move].effect == EFFECT_SNOWSCAPE)) return TRUE; - return FALSE; } @@ -3402,7 +3429,7 @@ bool32 PartnerMoveEffectIsTerrain(u32 battlerAtkPartner, u32 partnerMove) if (!IsDoubleBattle()) return FALSE; - if (gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE + if (partnerMove != MOVE_NONE && (gBattleMoves[partnerMove].effect == EFFECT_GRASSY_TERRAIN || gBattleMoves[partnerMove].effect == EFFECT_MISTY_TERRAIN || gBattleMoves[partnerMove].effect == EFFECT_ELECTRIC_TERRAIN @@ -3418,7 +3445,7 @@ bool32 PartnerMoveIs(u32 battlerAtkPartner, u32 partnerMove, u32 moveCheck) if (!IsDoubleBattle()) return FALSE; - if (gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE && partnerMove == moveCheck) + if (partnerMove != MOVE_NONE && partnerMove == moveCheck) return TRUE; return FALSE; } @@ -3429,7 +3456,7 @@ bool32 PartnerMoveIsSameAsAttacker(u32 battlerAtkPartner, u32 battlerDef, u32 mo if (!IsDoubleBattle()) return FALSE; - if (gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE && move == partnerMove && gBattleStruct->moveTarget[battlerAtkPartner] == battlerDef) + if (partnerMove != MOVE_NONE && move == partnerMove && gBattleStruct->moveTarget[battlerAtkPartner] == battlerDef) return TRUE; return FALSE; } @@ -3439,7 +3466,7 @@ bool32 PartnerMoveIsSameNoTarget(u32 battlerAtkPartner, u32 move, u32 partnerMov { if (!IsDoubleBattle()) return FALSE; - if (gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE && move == partnerMove) + if (partnerMove != MOVE_NONE && move == partnerMove) return TRUE; return FALSE; } diff --git a/src/battle_controller_opponent.c b/src/battle_controller_opponent.c index a74ca529b629..0ac5c38923e7 100644 --- a/src/battle_controller_opponent.c +++ b/src/battle_controller_opponent.c @@ -38,6 +38,7 @@ #include "constants/songs.h" #include "constants/trainers.h" #include "trainer_hill.h" +#include "test_runner.h" static void OpponentHandleLoadMonSprite(u32 battler); static void OpponentHandleSwitchInAnim(u32 battler); @@ -689,6 +690,9 @@ static void OpponentHandleChoosePokemon(u32 battler) *(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE; *(gBattleStruct->monToSwitchIntoId + battler) = chosenMonId; } + #if TESTING + TestRunner_Battle_CheckSwitch(battler, chosenMonId); + #endif // TESTING BtlController_EmitChosenMonReturnValue(battler, BUFFER_B, chosenMonId, NULL); OpponentBufferExecCompleted(battler); diff --git a/src/battle_controller_recorded_opponent.c b/src/battle_controller_recorded_opponent.c index 11f0fec8c51d..58ef101ec99a 100644 --- a/src/battle_controller_recorded_opponent.c +++ b/src/battle_controller_recorded_opponent.c @@ -437,25 +437,7 @@ static void RecordedOpponentHandleMoveAnimation(u32 battler) static void RecordedOpponentHandlePrintString(u32 battler) { - u16 *stringId; - - gBattle_BG0_X = 0; - gBattle_BG0_Y = 0; - stringId = (u16 *)(&gBattleResources->bufferA[battler][2]); - BufferStringBattle(*stringId, battler); - - if (gTestRunnerEnabled) - { - TestRunner_Battle_RecordMessage(gDisplayedStringBattle); - if (gTestRunnerHeadless) - { - RecordedOpponentBufferExecCompleted(battler); - return; - } - } - - BattlePutTextOnWindow(gDisplayedStringBattle, B_WIN_MSG); - gBattlerControllerFuncs[battler] = Controller_WaitForString; + BtlController_HandlePrintString(battler, FALSE, FALSE); } static void RecordedOpponentHandleChooseAction(u32 battler) diff --git a/src/battle_controller_recorded_player.c b/src/battle_controller_recorded_player.c index f372c0ff8881..39e869867efc 100644 --- a/src/battle_controller_recorded_player.c +++ b/src/battle_controller_recorded_player.c @@ -427,25 +427,7 @@ static void RecordedPlayerHandleMoveAnimation(u32 battler) static void RecordedPlayerHandlePrintString(u32 battler) { - u16 *stringId; - - gBattle_BG0_X = 0; - gBattle_BG0_Y = 0; - stringId = (u16 *)(&gBattleResources->bufferA[battler][2]); - BufferStringBattle(*stringId, battler); - - if (gTestRunnerEnabled) - { - TestRunner_Battle_RecordMessage(gDisplayedStringBattle); - if (gTestRunnerHeadless) - { - RecordedPlayerBufferExecCompleted(battler); - return; - } - } - - BattlePutTextOnWindow(gDisplayedStringBattle, B_WIN_MSG); - gBattlerControllerFuncs[battler] = Controller_WaitForString; + BtlController_HandlePrintString(battler, FALSE, FALSE); } static void ChooseActionInBattlePalace(u32 battler) diff --git a/src/battle_controllers.c b/src/battle_controllers.c index 026f433720a1..a10fbc0233de 100644 --- a/src/battle_controllers.c +++ b/src/battle_controllers.c @@ -2678,6 +2678,17 @@ void BtlController_HandlePrintString(u32 battler, bool32 updateTvData, bool32 ar gBattle_BG0_Y = 0; stringId = (u16 *)(&gBattleResources->bufferA[battler][2]); BufferStringBattle(*stringId, battler); + + if (gTestRunnerEnabled) + { + TestRunner_Battle_RecordMessage(gDisplayedStringBattle); + if (gTestRunnerHeadless) + { + BattleControllerComplete(battler); + return; + } + } + BattlePutTextOnWindow(gDisplayedStringBattle, B_WIN_MSG); gBattlerControllerFuncs[battler] = Controller_WaitForString; if (updateTvData) diff --git a/src/battle_main.c b/src/battle_main.c index 8461873f059b..75e44ef057c4 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -4376,6 +4376,11 @@ static void HandleTurnActionSelectionState(void) else if (gBattleResources->bufferB[battler][2] & RET_ULTRA_BURST) gBattleStruct->burst.toBurst |= gBitTable[battler]; gBattleCommunication[battler]++; + + if (gTestRunnerEnabled) + { + TestRunner_Battle_CheckChosenMove(battler, gChosenMoveByBattler[battler], gBattleStruct->moveTarget[battler]); + } } break; } diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 41749841a342..c0aff4f617b1 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -5097,9 +5097,15 @@ static void Cmd_playstatchangeanimation(void) // Handle Contrary and Simple if (ability == ABILITY_CONTRARY) + { flags ^= STAT_CHANGE_NEGATIVE; + RecordAbilityBattle(battler, ability); + } else if (ability == ABILITY_SIMPLE) + { flags |= STAT_CHANGE_BY_TWO; + RecordAbilityBattle(battler, ability); + } if (flags & STAT_CHANGE_NEGATIVE) // goes down { @@ -11419,6 +11425,7 @@ static u32 ChangeStatBuffs(s8 statValue, u32 statId, u32 flags, const u8 *BS_ptr { statValue ^= STAT_BUFF_NEGATIVE; gBattleScripting.statChanger ^= STAT_BUFF_NEGATIVE; + RecordAbilityBattle(battler, battlerAbility); if (flags & STAT_CHANGE_UPDATE_MOVE_EFFECT) { flags &= ~STAT_CHANGE_UPDATE_MOVE_EFFECT; diff --git a/src/battle_util.c b/src/battle_util.c index baeff398a9b7..5b8b1c32cdf7 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -6170,6 +6170,11 @@ bool32 IsMyceliumMightOnField(void) return FALSE; } +bool32 IsMoldBreakerTypeAbility(u32 ability) +{ + return (ability == ABILITY_MOLD_BREAKER || ability == ABILITY_TERAVOLT || ability == ABILITY_TURBOBLAZE); +} + u32 GetBattlerAbility(u32 battler) { if (gStatuses3[battler] & STATUS3_GASTRO_ACID) @@ -6181,9 +6186,7 @@ u32 GetBattlerAbility(u32 battler) if (IsMyceliumMightOnField()) return ABILITY_NONE; - if ((((gBattleMons[gBattlerAttacker].ability == ABILITY_MOLD_BREAKER - || gBattleMons[gBattlerAttacker].ability == ABILITY_TERAVOLT - || gBattleMons[gBattlerAttacker].ability == ABILITY_TURBOBLAZE) + if (((IsMoldBreakerTypeAbility(gBattleMons[gBattlerAttacker].ability) && !(gStatuses3[gBattlerAttacker] & STATUS3_GASTRO_ACID)) || gBattleMoves[gCurrentMove].ignoresTargetAbility) && sAbilitiesAffectedByMoldBreaker[gBattleMons[battler].ability] diff --git a/test/battle/ai.c b/test/battle/ai.c new file mode 100644 index 000000000000..47308df4deb1 --- /dev/null +++ b/test/battle/ai.c @@ -0,0 +1,407 @@ +#include "global.h" +#include "test/battle.h" +#include "battle_ai_util.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 + } +} + +AI_SINGLE_BATTLE_TEST("AI prefers Bubble over Water Gun if it's slower") +{ + u32 speedPlayer, speedAi; + + PARAMETRIZE { speedPlayer = 200; speedAi = 10; } + PARAMETRIZE { speedPlayer = 10; speedAi = 200; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_SCIZOR) { Speed(speedPlayer); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_WATER_GUN, MOVE_BUBBLE); Speed(speedAi); } + } WHEN { + if (speedPlayer > speedAi) + { + TURN { SCORE_GT(opponent, MOVE_BUBBLE, MOVE_WATER_GUN); } + TURN { SCORE_GT(opponent, MOVE_BUBBLE, MOVE_WATER_GUN); } + } + else + { + TURN { SCORE_EQ(opponent, MOVE_BUBBLE, MOVE_WATER_GUN); } + TURN { SCORE_EQ(opponent, MOVE_BUBBLE, MOVE_WATER_GUN); } + } + } +} + +AI_SINGLE_BATTLE_TEST("AI prefers Water Gun over Bubble if it knows that foe has Contrary") +{ + u32 abilityAI; + + PARAMETRIZE { abilityAI = ABILITY_MOXIE; } + PARAMETRIZE { abilityAI = ABILITY_MOLD_BREAKER; } // Mold Breaker ignores Contrary. + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_SHUCKLE) { Ability(ABILITY_CONTRARY); } + OPPONENT(SPECIES_PINSIR) { Moves(MOVE_WATER_GUN, MOVE_BUBBLE); Ability(abilityAI); } + } WHEN { + TURN { MOVE(player, MOVE_DEFENSE_CURL); } + TURN { MOVE(player, MOVE_DEFENSE_CURL); + if (abilityAI == ABILITY_MOLD_BREAKER) { SCORE_EQ(opponent, MOVE_WATER_GUN, MOVE_BUBBLE); } + else { SCORE_GT(opponent, MOVE_WATER_GUN, MOVE_BUBBLE); }} + } SCENE { + MESSAGE("Shuckle's Defense fell!"); // Contrary activates + } THEN { + EXPECT(gBattleResources->aiData->abilities[B_POSITION_PLAYER_LEFT] == ABILITY_CONTRARY); + } +} + +AI_SINGLE_BATTLE_TEST("AI prefers moves with better accuracy, but only if they both require the same number of hits to ko") +{ + u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE; + u16 hp, expectedMove, turns, abilityAtk, expectedMove2; + + abilityAtk = ABILITY_NONE; + expectedMove2 = MOVE_NONE; + + // Here it's a simple test, both Slam and Strength deal the same damage, but Strength always hits, whereas Slam often misses. + PARAMETRIZE { move1 = MOVE_SLAM; move2 = MOVE_STRENGTH; move3 = MOVE_TACKLE; hp = 490; expectedMove = MOVE_STRENGTH; turns = 4; } + PARAMETRIZE { move1 = MOVE_SLAM; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 365; expectedMove = MOVE_STRENGTH; turns = 3; } + PARAMETRIZE { move1 = MOVE_SLAM; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 245; expectedMove = MOVE_STRENGTH; turns = 2; } + PARAMETRIZE { move1 = MOVE_SLAM; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 125; expectedMove = MOVE_STRENGTH; turns = 1; } + // Mega Kick deals more damage, but can miss more often. Here, AI should choose Mega Kick if it can faint target in less number of turns than Strength. Otherwise, it should use Strength. + PARAMETRIZE { move1 = MOVE_MEGA_KICK; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 170; expectedMove = MOVE_MEGA_KICK; turns = 1; } + PARAMETRIZE { move1 = MOVE_MEGA_KICK; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 245; expectedMove = MOVE_STRENGTH; turns = 2; } + // Swift always hits and Guts has accuracy of 100%. Hustle lowers accuracy of all physical moves. + PARAMETRIZE { abilityAtk = ABILITY_HUSTLE; move1 = MOVE_MEGA_KICK; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 5; expectedMove = MOVE_SWIFT; turns = 1; } + PARAMETRIZE { abilityAtk = ABILITY_HUSTLE; move1 = MOVE_MEGA_KICK; move2 = MOVE_STRENGTH; move3 = MOVE_GUST; move4 = MOVE_TACKLE; hp = 5; expectedMove = MOVE_GUST; turns = 1; } + // Mega Kick and Slam both have lower accuracy. Gust and Tackle both have 100, so AI can choose either of them. + PARAMETRIZE { move1 = MOVE_MEGA_KICK; move2 = MOVE_SLAM; move3 = MOVE_TACKLE; move4 = MOVE_GUST; hp = 5; expectedMove = MOVE_GUST; expectedMove2 = MOVE_TACKLE; turns = 1; } + // All moves hit with No guard ability + PARAMETRIZE { move1 = MOVE_MEGA_KICK; move2 = MOVE_GUST; hp = 5; expectedMove = MOVE_MEGA_KICK; expectedMove2 = MOVE_GUST; turns = 1; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET) { HP(hp); } + PLAYER(SPECIES_WOBBUFFET); + ASSUME(gBattleMoves[MOVE_SWIFT].accuracy == 0); + ASSUME(gBattleMoves[MOVE_SLAM].power == gBattleMoves[MOVE_STRENGTH].power); + ASSUME(gBattleMoves[MOVE_MEGA_KICK].power > gBattleMoves[MOVE_STRENGTH].power); + ASSUME(gBattleMoves[MOVE_SLAM].accuracy < gBattleMoves[MOVE_STRENGTH].accuracy); + ASSUME(gBattleMoves[MOVE_MEGA_KICK].accuracy < gBattleMoves[MOVE_STRENGTH].accuracy); + ASSUME(gBattleMoves[MOVE_TACKLE].accuracy == 100); + ASSUME(gBattleMoves[MOVE_GUST].accuracy == 100); + OPPONENT(SPECIES_EXPLOUD) { Moves(move1, move2, move3, move4); Ability(abilityAtk); SpAttack(50); } // Low Sp.Atk, so Swift deals less damage than Strength. + } WHEN { + switch (turns) + { + case 1: + if (expectedMove2 != MOVE_NONE) { + TURN { EXPECT_MOVES(opponent, expectedMove, expectedMove2); SEND_OUT(player, 1); } + } + else { + TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + } + break; + case 2: + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + break; + case 3: + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + break; + case 4: + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + break; + } + } SCENE { + MESSAGE("Wobbuffet fainted!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI prefers moves which deal more damage instead of moves which are super-effective but deal less damage") +{ + u8 turns = 0; + u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE; + u16 expectedMove, abilityAtk, abilityDef, expectedMove2; + + abilityAtk = ABILITY_NONE; + expectedMove2 = MOVE_NONE; + + // Scald and Poison Jab take 3 hits, Waterfall takes 2. + PARAMETRIZE { move1 = MOVE_WATERFALL; move2 = MOVE_SCALD; move3 = MOVE_POISON_JAB; move4 = MOVE_WATER_GUN; expectedMove = MOVE_WATERFALL; turns = 2; } + // Poison Jab takes 3 hits, Water gun 5. Immunity so there's no poison chip damage. + PARAMETRIZE { move1 = MOVE_POISON_JAB; move2 = MOVE_WATER_GUN; expectedMove = MOVE_POISON_JAB; abilityDef = ABILITY_IMMUNITY; turns = 3; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_TYPHLOSION) { Ability(abilityDef); } + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_NIDOQUEEN) { Moves(move1, move2, move3, move4); Ability(abilityAtk); } + } WHEN { + switch (turns) + { + case 2: + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + break; + case 3: + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + break; + } + } SCENE { + MESSAGE("Typhlosion fainted!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI prefers Earthquake over Drill Run if both require the same number of hits to ko") +{ + // Drill Run has less accuracy than E-quake, but can score a higher crit. However the chance is too small, so AI should ignore it. + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_TYPHLOSION); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_GEODUDE) { Moves(MOVE_EARTHQUAKE, MOVE_DRILL_RUN); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } + TURN { EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); SEND_OUT(player, 1); } + } + SCENE { + MESSAGE("Typhlosion fainted!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI chooses the safest option to faint the target, taking into account accuracy and move effect") +{ + u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE; + u16 expectedMove, abilityAtk = ABILITY_NONE, abilityDef; + u16 expectedMove2 = MOVE_NONE; + + // Psychic is not very effective, but always hits. Solarbeam requires a charging turn, Double Edge has recoil and Focus Blast can miss; + PARAMETRIZE { abilityAtk = ABILITY_STURDY; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SOLAR_BEAM; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE; expectedMove = MOVE_PSYCHIC; } + // Same as above, but ai mon has rock head ability, so it can use Double Edge without taking recoil damage. Psychic can also lower Special Defense, + // but because it faints the target it doesn't matter. + PARAMETRIZE { abilityAtk = ABILITY_ROCK_HEAD; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SOLAR_BEAM; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE; + expectedMove = MOVE_PSYCHIC; expectedMove2 = MOVE_DOUBLE_EDGE; } + // This time it's Solarbeam + Psychic, because the weather is sunny. + PARAMETRIZE { abilityAtk = ABILITY_DROUGHT; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SOLAR_BEAM; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE; + expectedMove = MOVE_PSYCHIC; expectedMove2 = MOVE_SOLAR_BEAM; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET) { HP(5); } + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_GEODUDE) { Moves(move1, move2, move3, move4); Ability(abilityAtk); } + } WHEN { + TURN { if (expectedMove2 == MOVE_NONE) { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + else {EXPECT_MOVES(opponent, expectedMove, expectedMove2); SCORE_EQ(opponent, expectedMove, expectedMove2); SEND_OUT(player, 1);} + } + } + SCENE { + MESSAGE("Wobbuffet fainted!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI won't use ground type attacks against flying type Pokemon unless Gravity is in effect") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_CROBAT); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_NIDOQUEEN) { Moves(MOVE_EARTHQUAKE, MOVE_TACKLE, MOVE_POISON_STING, MOVE_GUST); } + } WHEN { + TURN { NOT_EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } + TURN { MOVE(player, MOVE_GRAVITY); NOT_EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } + TURN { EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); SEND_OUT(player, 1); } + } SCENE { + MESSAGE("Gravity intensified!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI will not switch in a Pokemon which is slower and gets 1HKOed after fainting") +{ + bool32 alakazamFaster; + u32 speedAlakazm; + + KNOWN_FAILING; + + PARAMETRIZE{ speedAlakazm = 200; alakazamFaster = FALSE; } + PARAMETRIZE{ speedAlakazm = 400; alakazamFaster = TRUE; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING); + PLAYER(SPECIES_WEAVILE) { Speed(300); Ability(ABILITY_SHADOW_TAG); } // Weavile has Shadow Tag, so AI can't switch on the first turn, but has to do it after fainting. + OPPONENT(SPECIES_KADABRA) { Speed(200); Moves(MOVE_PSYCHIC, MOVE_DISABLE, MOVE_TAUNT, MOVE_CALM_MIND); } + OPPONENT(SPECIES_ALAKAZAM) { Speed(speedAlakazm); Moves(MOVE_FOCUS_BLAST, MOVE_PSYCHIC); } // Alakazam has a move which OHKOes Weavile, but it doesn't matter if he's getting KO-ed first. + OPPONENT(SPECIES_BLASTOISE) { Speed(200); Moves(MOVE_BUBBLE_BEAM, MOVE_WATER_GUN, MOVE_LEER, MOVE_STRENGTH); } // Can't OHKO, but survives a hit from Weavile's Night Slash. + } WHEN { + TURN { MOVE(player, MOVE_NIGHT_SLASH) ; EXPECT_SEND_OUT(opponent, alakazamFaster ? 1 : 2); } + } SCENE { + MESSAGE("Foe Kadabra fainted!"); + if (alakazamFaster) { + MESSAGE("{PKMN} TRAINER LEAF sent out Alakazam!"); + } else { + MESSAGE("{PKMN} TRAINER LEAF sent out Blastoise!"); + } + } +} + +AI_SINGLE_BATTLE_TEST("AI switches if Perish Song is about to kill") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) {Moves(MOVE_TACKLE); } + OPPONENT(SPECIES_CROBAT) {Moves(MOVE_TACKLE); } + } WHEN { + TURN { MOVE(player, MOVE_PERISH_SONG); } + TURN { ; } + TURN { ; } + TURN { EXPECT_SWITCH(opponent, 1); } + } SCENE { + MESSAGE("{PKMN} TRAINER LEAF sent out Crobat!"); + } +} + +AI_DOUBLE_BATTLE_TEST("AI won't use a Weather changing move if partner already chose such move") +{ + u32 j, k; + static const u16 weatherMoves[] = {MOVE_SUNNY_DAY, MOVE_HAIL, MOVE_RAIN_DANCE, MOVE_SANDSTORM, MOVE_SNOWSCAPE}; + u16 weatherMoveLeft, weatherMoveRight; + + for (j = 0; j < ARRAY_COUNT(weatherMoves); j++) + { + for (k = 0; k < ARRAY_COUNT(weatherMoves); k++) + { + PARAMETRIZE { weatherMoveLeft = weatherMoves[j]; weatherMoveRight = weatherMoves[k]; } + } + } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(weatherMoveLeft); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_TACKLE, weatherMoveRight); } + } WHEN { + TURN { NOT_EXPECT_MOVE(opponentRight, weatherMoveRight); + SCORE_LT_VAL(opponentRight, weatherMoveRight, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_LT_VAL(opponentRight, weatherMoveRight, AI_SCORE_DEFAULT, target:playerRight); + SCORE_LT_VAL(opponentRight, weatherMoveRight, AI_SCORE_DEFAULT, target:opponentLeft); + } + } +} + +AI_DOUBLE_BATTLE_TEST("AI will not use Helping Hand if partner does not have any damage moves") +{ + u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE; + + PARAMETRIZE{ move1 = MOVE_LEER; move2 = MOVE_TOXIC; } + PARAMETRIZE{ move1 = MOVE_HELPING_HAND; move2 = MOVE_PROTECT; } + PARAMETRIZE{ move1 = MOVE_ACUPRESSURE; move2 = MOVE_DOUBLE_TEAM; move3 = MOVE_TOXIC; move4 = MOVE_PROTECT; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_HELPING_HAND, MOVE_TACKLE); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(move1, move2, move3, move4); } + } WHEN { + TURN { NOT_EXPECT_MOVE(opponentLeft, MOVE_HELPING_HAND); + SCORE_LT_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_LT_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:playerRight); + SCORE_LT_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:opponentLeft); + } + } SCENE { + NOT MESSAGE("Foe Wobbuffet used Helping Hand!"); + } +} + +AI_DOUBLE_BATTLE_TEST("AI will not use a status move if partner already chose Helping Hand") +{ + s32 j; + u32 statusMove; + + for (j = MOVE_NONE + 1; j < MOVES_COUNT; j++) + { + if (gBattleMoves[j].split == SPLIT_STATUS) { + PARAMETRIZE{ statusMove = j; } + } + } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_HELPING_HAND); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_TACKLE, statusMove); } + } WHEN { + TURN { NOT_EXPECT_MOVE(opponentRight, statusMove); + SCORE_LT_VAL(opponentRight, statusMove, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_LT_VAL(opponentRight, statusMove, AI_SCORE_DEFAULT, target:playerRight); + SCORE_LT_VAL(opponentRight, statusMove, AI_SCORE_DEFAULT, target:opponentLeft); + } + } SCENE { + MESSAGE("Foe Wobbuffet used Helping Hand!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI without any flags chooses moves at random - singles") +{ + GIVEN { + AI_FLAGS(0); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_NIDOQUEEN) { Moves(MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); } + } WHEN { + TURN { EXPECT_MOVES(opponent, MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); + SCORE_EQ_VAL(opponent, MOVE_SPLASH, AI_SCORE_DEFAULT); + SCORE_EQ_VAL(opponent, MOVE_EXPLOSION, AI_SCORE_DEFAULT); + SCORE_EQ_VAL(opponent, MOVE_RAGE, AI_SCORE_DEFAULT); + SCORE_EQ_VAL(opponent, MOVE_HELPING_HAND, AI_SCORE_DEFAULT); + } + } +} + +AI_DOUBLE_BATTLE_TEST("AI without any flags chooses moves at random - doubles") +{ + GIVEN { + AI_FLAGS(0); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_NIDOQUEEN) { Moves(MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); } + OPPONENT(SPECIES_NIDOQUEEN) { Moves(MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); } + } WHEN { + TURN { EXPECT_MOVES(opponentLeft, MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); + EXPECT_MOVES(opponentRight, MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); + SCORE_EQ_VAL(opponentLeft, MOVE_SPLASH, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_EQ_VAL(opponentLeft, MOVE_EXPLOSION, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_EQ_VAL(opponentLeft, MOVE_RAGE, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_EQ_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_EQ_VAL(opponentRight, MOVE_SPLASH, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_EQ_VAL(opponentRight, MOVE_EXPLOSION, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_EQ_VAL(opponentRight, MOVE_RAGE, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_EQ_VAL(opponentRight, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:playerLeft); + } + } +} diff --git a/test/test_runner_battle.c b/test/test_runner_battle.c index 2d7d14833e1c..01a8953ba00d 100644 --- a/test/test_runner_battle.c +++ b/test/test_runner_battle.c @@ -1,5 +1,6 @@ #include "global.h" #include "battle.h" +#include "battle_ai_util.h" #include "battle_anim.h" #include "battle_controllers.h" #include "characters.h" @@ -38,6 +39,9 @@ EWRAM_DATA struct BattleTestRunnerState *gBattleTestRunnerState = NULL; static void CB2_BattleTest_NextParameter(void); static void CB2_BattleTest_NextTrial(void); static void PushBattlerAction(u32 sourceLine, s32 battlerId, u32 actionType, u32 byte); +static void PrintAiMoveLog(u32 battlerId, u32 moveSlot, u32 moveId, s32 totalScore); +static void ClearAiLog(u32 battlerId); +static const char *BattlerIdentifier(s32 battlerId); NAKED static void InvokeSingleTestFunctionWithStack(void *results, u32 i, struct BattlePokemon *player, struct BattlePokemon *opponent, SingleBattleTestFunction function, void *stack) { @@ -96,23 +100,42 @@ static void InvokeTestFunction(const struct BattleTest *test) { case BATTLE_TEST_SINGLES: case BATTLE_TEST_WILD: + case BATTLE_TEST_AI_SINGLES: InvokeSingleTestFunctionWithStack(STATE->results, STATE->runParameter, &gBattleMons[B_POSITION_PLAYER_LEFT], &gBattleMons[B_POSITION_OPPONENT_LEFT], test->function.singles, &DATA.stack[BATTLE_TEST_STACK_SIZE]); break; case BATTLE_TEST_DOUBLES: + case BATTLE_TEST_AI_DOUBLES: InvokeDoubleTestFunctionWithStack(STATE->results, STATE->runParameter, &gBattleMons[B_POSITION_PLAYER_LEFT], &gBattleMons[B_POSITION_OPPONENT_LEFT], &gBattleMons[B_POSITION_PLAYER_RIGHT], &gBattleMons[B_POSITION_OPPONENT_RIGHT], test->function.singles, &DATA.stack[BATTLE_TEST_STACK_SIZE]); break; } } -static u32 SourceLine(u32 sourceLineOffset) +static const struct BattleTest *GetBattleTest(void) { const struct BattleTest *test = gTestRunnerState.test->data; + return test; +} + +static bool32 IsAITest(void) +{ + switch (GetBattleTest()->type) + { + case BATTLE_TEST_AI_SINGLES: + case BATTLE_TEST_AI_DOUBLES: + return TRUE; + } + return FALSE; +} + +static u32 SourceLine(u32 sourceLineOffset) +{ + const struct BattleTest *test = GetBattleTest(); return test->sourceLine + sourceLineOffset; } static u32 SourceLineOffset(u32 sourceLine) { - const struct BattleTest *test = gTestRunnerState.test->data; + const struct BattleTest *test = GetBattleTest(); if (sourceLine - test->sourceLine > 0xFF) return 0; else @@ -155,9 +178,12 @@ static void BattleTest_SetUp(void *data) switch (test->type) { case BATTLE_TEST_SINGLES: + case BATTLE_TEST_WILD: + case BATTLE_TEST_AI_SINGLES: STATE->battlersCount = 2; break; case BATTLE_TEST_DOUBLES: + case BATTLE_TEST_AI_DOUBLES: STATE->battlersCount = 4; break; } @@ -234,18 +260,35 @@ static void BattleTest_Run(void *data) memset(&DATA, 0, sizeof(DATA)); DATA.recordedBattle.rngSeed = RNG_SEED_DEFAULT; - DATA.recordedBattle.opponentA = TRAINER_LINK_OPPONENT; - DATA.recordedBattle.textSpeed = OPTIONS_TEXT_SPEED_FAST; - if (test->type == BATTLE_TEST_WILD) - DATA.recordedBattle.battleFlags = BATTLE_TYPE_IS_MASTER; - else - DATA.recordedBattle.battleFlags = BATTLE_TYPE_RECORDED_IS_MASTER | BATTLE_TYPE_RECORDED_LINK | BATTLE_TYPE_TRAINER | BATTLE_TYPE_IS_MASTER; - if (test->type == BATTLE_TEST_DOUBLES) + // Set battle flags and opponent ids. + switch (test->type) { - DATA.recordedBattle.battleFlags |= BATTLE_TYPE_DOUBLE; + case BATTLE_TEST_WILD: + DATA.recordedBattle.battleFlags = BATTLE_TYPE_IS_MASTER; + break; + case BATTLE_TEST_AI_SINGLES: + DATA.recordedBattle.battleFlags = BATTLE_TYPE_IS_MASTER | BATTLE_TYPE_TRAINER; + DATA.recordedBattle.opponentA = TRAINER_LEAF; + DATA.hasAI = TRUE; + break; + case BATTLE_TEST_AI_DOUBLES: + DATA.recordedBattle.battleFlags = BATTLE_TYPE_IS_MASTER | BATTLE_TYPE_TRAINER | BATTLE_TYPE_DOUBLE; + DATA.recordedBattle.opponentA = TRAINER_LEAF; + DATA.recordedBattle.opponentB = TRAINER_RED; + DATA.hasAI = TRUE; + break; + case BATTLE_TEST_SINGLES: + DATA.recordedBattle.battleFlags = BATTLE_TYPE_IS_MASTER | BATTLE_TYPE_RECORDED_IS_MASTER | BATTLE_TYPE_RECORDED_LINK | BATTLE_TYPE_TRAINER; + DATA.recordedBattle.opponentA = TRAINER_LINK_OPPONENT; + break; + case BATTLE_TEST_DOUBLES: + DATA.recordedBattle.battleFlags = BATTLE_TYPE_IS_MASTER | BATTLE_TYPE_RECORDED_IS_MASTER | BATTLE_TYPE_RECORDED_LINK | BATTLE_TYPE_TRAINER | BATTLE_TYPE_DOUBLE; + DATA.recordedBattle.opponentA = TRAINER_LINK_OPPONENT; DATA.recordedBattle.opponentB = TRAINER_LINK_OPPONENT; + break; } + for (i = 0; i < STATE->battlersCount; i++) { DATA.recordedBattle.playersName[i][0] = CHAR_1 + i; @@ -711,6 +754,292 @@ void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP) } } +static const char *const sBattleActionNames[] = +{ + [B_ACTION_USE_MOVE] = "MOVE", + [B_ACTION_USE_ITEM] = "USE_ITEM", + [B_ACTION_SWITCH] = "SWITCH", +}; + +static u32 CountAiExpectMoves(struct ExpectedAIAction *expectedAction, u32 battlerId, bool32 printLog) +{ + u32 i, countExpected = 0; + for (i = 0; i < MAX_MON_MOVES; i++) + { + if (gBitTable[i] & expectedAction->moveSlots) + { + if (printLog) + PrintAiMoveLog(battlerId, i, gBattleMons[battlerId].moves[i], gBattleStruct->aiFinalScore[battlerId][expectedAction->target][i]); + countExpected++; + } + } + return countExpected; +} + +void TestRunner_Battle_CheckChosenMove(u32 battlerId, u32 moveId, u32 target) +{ + const char *filename = gTestRunnerState.test->filename; + u32 id = DATA.aiActionsPlayed[battlerId]; + struct ExpectedAIAction *expectedAction = &DATA.expectedAiActions[battlerId][id]; + + if (!expectedAction->actionSet) + return; + + if (!expectedAction->pass) + { + u32 i, expectedMoveId, countExpected; + bool32 movePasses = FALSE; + + if (expectedAction->type != B_ACTION_USE_MOVE) + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Expected MOVE, got %s", filename, expectedAction->sourceLine, sBattleActionNames[expectedAction->type]); + + if (expectedAction->explicitTarget && expectedAction->target != target) + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Expected target %s, got %s", filename, expectedAction->sourceLine, BattlerIdentifier(expectedAction->target), BattlerIdentifier(target)); + + for (i = 0; i < MAX_MON_MOVES; i++) + { + if (gBitTable[i] & expectedAction->moveSlots) + { + expectedMoveId = gBattleMons[battlerId].moves[i]; + if (!expectedAction->notMove) + { + if (moveId == expectedMoveId) + { + movePasses = TRUE; + break; + } + } + else + { + if (moveId == expectedMoveId) + { + movePasses = FALSE; + break; + } + movePasses = TRUE; + } + } + } + + countExpected = CountAiExpectMoves(expectedAction, battlerId, TRUE); + + if (!expectedAction->notMove && !movePasses) + { + u32 moveSlot = GetMoveSlot(gBattleMons[battlerId].moves, moveId); + PrintAiMoveLog(battlerId, moveSlot, moveId, gBattleStruct->aiFinalScore[battlerId][expectedAction->target][moveSlot]); + if (countExpected > 1) + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Unmatched EXPECT_MOVES %S, got %S", filename, expectedAction->sourceLine, gMoveNames[expectedMoveId], gMoveNames[moveId]); + else + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Unmatched EXPECT_MOVE %S, got %S", filename, expectedAction->sourceLine, gMoveNames[expectedMoveId], gMoveNames[moveId]); + } + if (expectedAction->notMove && !movePasses) + { + if (countExpected > 1) + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Unmatched NOT_EXPECT_MOVES %S", filename, expectedAction->sourceLine, gMoveNames[expectedMoveId]); + else + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Unmatched NOT_EXPECT_MOVE %S", filename, expectedAction->sourceLine, gMoveNames[expectedMoveId]); + } + } + // Turn passed, clear logs from the turn + ClearAiLog(battlerId); + DATA.aiActionsPlayed[battlerId]++; +} + +void TestRunner_Battle_CheckSwitch(u32 battlerId, u32 partyIndex) +{ + const char *filename = gTestRunnerState.test->filename; + u32 id = DATA.aiActionsPlayed[battlerId]; + struct ExpectedAIAction *expectedAction = &DATA.expectedAiActions[battlerId][id]; + + if (!expectedAction->actionSet) + return; + + if (!expectedAction->pass) + { + if (expectedAction->type != B_ACTION_SWITCH) + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Expected SWITCH/SEND_OUT, got %s", filename, expectedAction->sourceLine, sBattleActionNames[expectedAction->type]); + + if (expectedAction->target != partyIndex) + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Expected partyIndex %d, got %d", filename, expectedAction->sourceLine, expectedAction->target, partyIndex); + } + DATA.aiActionsPlayed[battlerId]++; +} + +static bool32 CheckComparision(s32 val1, s32 val2, u32 cmp) +{ + switch (cmp) + { + case CMP_EQUAL: + return (val1 == val2); + case CMP_NOT_EQUAL: + return (val1 != val2); + case CMP_GREATER_THAN: + return (val1 > val2); + case CMP_LESS_THAN: + return (val1 < val2); + } + return FALSE; +} + +static const char *const sCmpToStringTable[] = +{ + [CMP_EQUAL] = "EQ", + [CMP_NOT_EQUAL] = "NE", + [CMP_LESS_THAN] = "LT", + [CMP_GREATER_THAN] = "GT", +}; + +static void CheckIfMaxScoreEqualExpectMove(u32 battlerId, s32 target, struct ExpectedAIAction *aiAction, const char *filename) +{ + u32 i; + s32 *scores = gBattleStruct->aiFinalScore[battlerId][target]; + s32 bestScore = 0, bestScoreId = 0; + u16 *moves = gBattleMons[battlerId].moves; + for (i = 0; i < MAX_MON_MOVES; i++) + { + if (scores[i] > bestScore) + { + bestScore = scores[i]; + bestScoreId = i; + } + } + for (i = 0; i < MAX_MON_MOVES; i++) + { + // We expect move 'i', but it has the same best score as another move that we didn't expect. + if (scores[i] == scores[bestScoreId] + && !aiAction->notMove + && (aiAction->moveSlots & gBitTable[i]) + && !(aiAction->moveSlots & gBitTable[bestScoreId])) + { + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_MOVE %S has the same best score(%d) as not expected MOVE %S", filename, + aiAction->sourceLine, gMoveNames[moves[i]], scores[i], gMoveNames[moves[bestScoreId]]); + } + // We DO NOT expect move 'i', but it has the same best score as another move. + if (scores[i] == scores[bestScoreId] + && aiAction->notMove + && (aiAction->moveSlots & gBitTable[i]) + && !(aiAction->moveSlots & gBitTable[bestScoreId])) + { + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: NOT_EXPECT_MOVE %S has the same best score(%d) as MOVE %S", filename, + aiAction->sourceLine, gMoveNames[moves[i]], scores[i], gMoveNames[moves[bestScoreId]]); + } + } +} + +static void PrintAiMoveLog(u32 battlerId, u32 moveSlot, u32 moveId, s32 totalScore) +{ + s32 i, scoreFromLogs = 0; + + if (!DATA.logAI) return; + if (DATA.aiLogPrintedForMove[battlerId] & gBitTable[moveSlot]) return; + + DATA.aiLogPrintedForMove[battlerId] |= gBitTable[moveSlot]; + MgbaPrintf_("Score Log for move %S:\n", gMoveNames[moveId]); + for (i = 0; i < MAX_AI_LOG_LINES; i++) + { + struct AILogLine *log = &DATA.aiLogLines[battlerId][moveSlot][i]; + if (log->file) + { + if (log->set) + { + scoreFromLogs = log->score; + MgbaPrintf_("%s:%d: = %d\n", log->file, log->line, log->score); + } + else if (log->score > 0) + { + scoreFromLogs += log->score; + MgbaPrintf_("%s:%d: +%d\n", log->file, log->line, log->score); + } + else + { + scoreFromLogs += log->score; + MgbaPrintf_("%s:%d: %d\n", log->file, log->line, log->score); + } + } + else + { + break; + } + } + if (scoreFromLogs != totalScore) + { + Test_ExitWithResult(TEST_RESULT_ERROR, "Warning! Score from logs(%d) is different than actual score(%d). Make sure all of the score adjustments use the ADJUST_SCORE macro\n", scoreFromLogs, totalScore); + } + MgbaPrintf_("Total: %d\n", totalScore); +} + +static void ClearAiLog(u32 battlerId) +{ + u32 i, j; + for (i = 0; i < MAX_MON_MOVES; i++) + { + struct AILogLine *logs = DATA.aiLogLines[battlerId][i]; + for (j = 0; j < MAX_AI_LOG_LINES; j++) + memset(&logs[j], 0, sizeof(struct AILogLine)); + } + DATA.aiLogPrintedForMove[battlerId] = 0; +} + +void TestRunner_Battle_CheckAiMoveScores(u32 battlerId) +{ + s32 i; + struct ExpectedAIAction *aiAction; + const char *filename = gTestRunnerState.test->filename; + s32 turn = gBattleResults.battleTurnCounter; + + for (i = 0; i < MAX_AI_SCORE_COMPARISION_PER_TURN; i++) + { + struct ExpectedAiScore *scoreCtx = &DATA.expectedAiScores[battlerId][turn][i]; + if (scoreCtx->set) + { + u32 moveId1 = gBattleMons[battlerId].moves[scoreCtx->moveSlot1]; + s32 target = scoreCtx->target; + s32 *scores = gBattleStruct->aiFinalScore[battlerId][target]; + + if (scoreCtx->toValue) + { + PrintAiMoveLog(battlerId, scoreCtx->moveSlot1, moveId1, scores[scoreCtx->moveSlot1]); + if (!CheckComparision(scores[scoreCtx->moveSlot1], scoreCtx->value, scoreCtx->cmp)) + { + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Unmatched SCORE_%s_VAL %S %d, got %d", + filename, scoreCtx->sourceLine, sCmpToStringTable[scoreCtx->cmp], gMoveNames[moveId1], scoreCtx->value, scores[scoreCtx->moveSlot1]); + } + } + else + { + u32 moveId2 = gBattleMons[battlerId].moves[scoreCtx->moveSlot2]; + PrintAiMoveLog(battlerId, scoreCtx->moveSlot1, moveId1, scores[scoreCtx->moveSlot1]); + PrintAiMoveLog(battlerId, scoreCtx->moveSlot2, moveId2, scores[scoreCtx->moveSlot2]); + if (!CheckComparision(scores[scoreCtx->moveSlot1], scores[scoreCtx->moveSlot2], scoreCtx->cmp)) + { + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Unmatched SCORE_%s, got %S: %d, %S: %d", + filename, scoreCtx->sourceLine, sCmpToStringTable[scoreCtx->cmp], gMoveNames[moveId1], scores[scoreCtx->moveSlot1], gMoveNames[moveId2], scores[scoreCtx->moveSlot2]); + } + } + } + } + + // We need to make sure that the expected move has the best score. We have to rule out a situation where the expected move is used, but it has the same number of points as some other moves. + aiAction = &DATA.expectedAiActions[battlerId][DATA.aiActionsPlayed[battlerId]]; + if (aiAction->actionSet && !aiAction->pass) + { + s32 target = aiAction->target; + // AI's move targets self, but points for this move are distributed for all other battlers + if (aiAction->target == battlerId) + { + for (i = 0; i < MAX_BATTLERS_COUNT; i++) + { + if (i != battlerId && IsBattlerAlive(i)) + CheckIfMaxScoreEqualExpectMove(battlerId, i, aiAction, filename); + } + } + else + { + CheckIfMaxScoreEqualExpectMove(battlerId, target, aiAction, filename); + } + } +} + static s32 TryExp(s32 i, s32 n, u32 battlerId, u32 oldExp, u32 newExp) { struct QueuedExpEvent *event; @@ -812,6 +1141,7 @@ static s32 TryMessage(s32 i, s32 n, const u8 *string) continue; event = &DATA.queuedEvents[i].as.message; + // MgbaPrintf_("Looking for: %S Found: %S\n", event->pattern, string); // Useful for debugging. for (j = k = 0; ; j++, k++) { if (event->pattern[k] == CHAR_SPACE) @@ -966,7 +1296,7 @@ static const char *const sEventTypeMacros[] = void TestRunner_Battle_AfterLastTurn(void) { - const struct BattleTest *test = gTestRunnerState.test->data; + const struct BattleTest *test = GetBattleTest(); if (DATA.turns - 1 != DATA.lastActionTurn) { @@ -1099,7 +1429,7 @@ static bool32 BattleTest_HandleExitWithResult(void *data, enum TestResult result void Randomly(u32 sourceLine, u32 passes, u32 trials, struct RandomlyContext ctx) { - const struct BattleTest *test = gTestRunnerState.test->data; + const struct BattleTest *test = GetBattleTest(); INVALID_IF(STATE->trials != 0, "PASSES_RANDOMLY can only be used once per test"); INVALID_IF(test->resultsSize > 0, "PASSES_RANDOMLY is incompatible with results"); INVALID_IF(passes > trials, "%d passes specified, but only %d trials", passes, trials); @@ -1128,6 +1458,19 @@ void RNGSeed_(u32 sourceLine, u32 seed) DATA.recordedBattle.rngSeed = seed; } +void AIFlags_(u32 sourceLine, u32 flags) +{ + INVALID_IF(!IsAITest(), "AI_FLAGS is usable only in AI_SINGLE_BATTLE_TEST & AI_DOUBLE_BATTLE_TEST"); + DATA.recordedBattle.AI_scripts = flags; + DATA.hasAI = TRUE; +} + +void AILogScores(u32 sourceLine) +{ + INVALID_IF(!IsAITest(), "AI_LOG is usable only in AI_SINGLE_BATTLE_TEST & AI_DOUBLE_BATTLE_TEST"); + DATA.logAI = TRUE; +} + const struct TestRunner gBattleTestRunner = { .estimateCost = BattleTest_EstimateCost, @@ -1327,7 +1670,7 @@ void Item_(u32 sourceLine, u32 item) SetMonData(DATA.currentMon, MON_DATA_HELD_ITEM, &item); } -void Moves_(u32 sourceLine, const u16 moves[MAX_MON_MOVES]) +void Moves_(u32 sourceLine, u16 moves[MAX_MON_MOVES]) { s32 i; INVALID_IF(!DATA.currentMon, "Moves outside of PLAYER/OPPONENT"); @@ -1392,11 +1735,16 @@ static const char *const sBattlerIdentifiersDoubles[] = static const char *BattlerIdentifier(s32 battlerId) { - const struct BattleTest *test = gTestRunnerState.test->data; + const struct BattleTest *test = GetBattleTest(); switch (test->type) { - case BATTLE_TEST_SINGLES: return sBattlerIdentifiersSingles[battlerId]; - case BATTLE_TEST_DOUBLES: return sBattlerIdentifiersDoubles[battlerId]; + case BATTLE_TEST_SINGLES: + case BATTLE_TEST_WILD: + case BATTLE_TEST_AI_SINGLES: + return sBattlerIdentifiersSingles[battlerId]; + case BATTLE_TEST_DOUBLES: + case BATTLE_TEST_AI_DOUBLES: + return sBattlerIdentifiersDoubles[battlerId]; } return ""; } @@ -1446,18 +1794,7 @@ void TestRunner_Battle_CheckBattleRecordActionType(u32 battlerId, u32 recordInde switch (DATA.battleRecordTypes[battlerId][recordIndex]) { case RECORDED_ACTION_TYPE: - switch (DATA.recordedBattle.battleRecord[battlerId][recordIndex]) - { - case B_ACTION_USE_MOVE: - actualMacro = "MOVE"; - break; - case B_ACTION_SWITCH: - actualMacro = "SWITCH"; - break; - case B_ACTION_USE_ITEM: - actualMacro = "USE_ITEM"; - break; - } + actualMacro = sBattleActionNames[DATA.recordedBattle.battleRecord[battlerId][recordIndex]]; break; case RECORDED_PARTY_INDEX: actualMacro = "SEND_OUT"; @@ -1517,15 +1854,30 @@ static void SetSlowerThan(s32 battlerId) DATA.slowerThan[battlerId & BIT_SIDE][DATA.currentMonIndexes[battlerId]] |= slowerThan; } +static void SetAiActionToPass(u32 sourceLine, s32 battlerId) +{ + DATA.expectedAiActions[battlerId][DATA.expectedAiActionIndex[battlerId]].actionSet = TRUE; + DATA.expectedAiActions[battlerId][DATA.expectedAiActionIndex[battlerId]].sourceLine = sourceLine; + DATA.expectedAiActions[battlerId][DATA.expectedAiActionIndex[battlerId]].pass = TRUE; + DATA.expectedAiActionIndex[battlerId]++; +} + void CloseTurn(u32 sourceLine) { s32 i; INVALID_IF(DATA.turnState != TURN_OPEN, "Nested TURN"); DATA.turnState = TURN_CLOSING; + + // If Move was not specified always use Celebrate. In AI Tests allow any taken action. for (i = 0; i < STATE->battlersCount; i++) { if (!(DATA.actionBattlers & (1 << i))) - Move(sourceLine, &gBattleMons[i], (struct MoveContext) { move: MOVE_CELEBRATE, explicitMove: TRUE }); + { + if (IsAITest() && (i & BIT_SIDE) == B_SIDE_OPPONENT) // If Move was not specified, allow any move used. + SetAiActionToPass(sourceLine, i); + else + Move(sourceLine, &gBattleMons[i], (struct MoveContext) { move: MOVE_CELEBRATE, explicitMove: TRUE }); + } } DATA.turnState = TURN_CLOSED; DATA.turns++; @@ -1541,59 +1893,12 @@ static struct Pokemon *CurrentMon(s32 battlerId) return &party[DATA.currentMonIndexes[battlerId]]; } -void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx) +s32 MoveGetTarget(s32 battlerId, u32 moveId, struct MoveContext *ctx, u32 sourceLine) { - s32 i; - s32 battlerId = battler - gBattleMons; - struct Pokemon *mon = CurrentMon(battlerId); - u32 moveId, moveSlot; - s32 target; - - INVALID_IF(DATA.turnState == TURN_CLOSED, "MOVE outside TURN"); - - if (ctx.explicitMove) - { - INVALID_IF(ctx.move == MOVE_NONE || ctx.move >= MOVES_COUNT, "Illegal move: %d", ctx.move); - for (i = 0; i < MAX_MON_MOVES; i++) - { - moveId = GetMonData(mon, MON_DATA_MOVE1 + i); - if (moveId == ctx.move) - { - moveSlot = i; - break; - } - else if (moveId == MOVE_NONE) - { - INVALID_IF(DATA.explicitMoves[battlerId & BIT_SIDE] & (1 << DATA.currentMonIndexes[battlerId]), "Missing explicit %S", gMoveNames[ctx.move]); - SetMonData(mon, MON_DATA_MOVE1 + i, &ctx.move); - SetMonData(DATA.currentMon, MON_DATA_PP1 + i, &gBattleMoves[ctx.move].pp); - moveSlot = i; - moveId = ctx.move; - break; - } - } - INVALID_IF(i == MAX_MON_MOVES, "Too many different moves for %s", BattlerIdentifier(battlerId)); - } - else if (ctx.explicitMoveSlot) - { - moveSlot = ctx.moveSlot; - moveId = GetMonData(mon, MON_DATA_MOVE1 + moveSlot); - INVALID_IF(moveId == MOVE_NONE, "Empty moveSlot: %d", ctx.moveSlot); - } - else - { - INVALID("No move or moveSlot"); - } - - if (ctx.explicitMegaEvolve && ctx.megaEvolve) - moveSlot |= RET_MEGA_EVOLUTION; - - if (ctx.explicitUltraBurst && ctx.ultraBurst) - moveSlot |= RET_ULTRA_BURST; - - if (ctx.explicitTarget) + s32 target = battlerId; + if (ctx->explicitTarget) { - target = ctx.target - gBattleMons; + target = ctx->target - gBattleMons; } else { @@ -1609,7 +1914,11 @@ void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx) } else if (move->target == MOVE_TARGET_SELECTED) { - INVALID_IF(STATE->battlersCount > 2, "%S requires explicit target", gMoveNames[moveId]); + // In AI Doubles not specified target allows any target for EXPECT_MOVE. + if (GetBattleTest()->type != BATTLE_TEST_AI_DOUBLES) + { + INVALID_IF(STATE->battlersCount > 2, "%S requires explicit target", gMoveNames[moveId]); + } target = BATTLE_OPPOSITE(battlerId); } @@ -1623,9 +1932,74 @@ void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx) } else { - INVALID("%S requires explicit target", gMoveNames[moveId]); + // In AI Doubles not specified target allows any target for EXPECT_MOVE. + if (GetBattleTest()->type != BATTLE_TEST_AI_DOUBLES) + { + INVALID("%S requires explicit target", gMoveNames[moveId]); + } } } + return target; +} + +void MoveGetIdAndSlot(s32 battlerId, struct MoveContext *ctx, u32 *moveId, u32 *moveSlot, u32 sourceLine) +{ + u32 i; + struct Pokemon *mon = CurrentMon(battlerId); + + if (ctx->explicitMove) + { + INVALID_IF(ctx->move == MOVE_NONE || ctx->move >= MOVES_COUNT, "Illegal move: %d", ctx->move); + for (i = 0; i < MAX_MON_MOVES; i++) + { + *moveId = GetMonData(mon, MON_DATA_MOVE1 + i); + if (*moveId == ctx->move) + { + *moveSlot = i; + break; + } + else if (*moveId == MOVE_NONE) + { + INVALID_IF(DATA.explicitMoves[battlerId & BIT_SIDE] & (1 << DATA.currentMonIndexes[battlerId]), "Missing explicit %S", gMoveNames[ctx->move]); + SetMonData(mon, MON_DATA_MOVE1 + i, &ctx->move); + SetMonData(DATA.currentMon, MON_DATA_PP1 + i, &gBattleMoves[ctx->move].pp); + *moveSlot = i; + *moveId = ctx->move; + break; + } + } + INVALID_IF(i == MAX_MON_MOVES, "Too many different moves for %s", BattlerIdentifier(battlerId)); + } + else if (ctx->explicitMoveSlot) + { + *moveSlot = ctx->moveSlot; + *moveId = GetMonData(mon, MON_DATA_MOVE1 + *moveSlot); + INVALID_IF(moveId == MOVE_NONE, "Empty moveSlot: %d", ctx->moveSlot); + } + else + { + INVALID("No move or moveSlot"); + } + + if (ctx->explicitMegaEvolve && ctx->megaEvolve) + *moveSlot |= RET_MEGA_EVOLUTION; + + if (ctx->explicitUltraBurst && ctx->ultraBurst) + *moveSlot |= RET_ULTRA_BURST; +} + +void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx) +{ + s32 i; + s32 battlerId = battler - gBattleMons; + u32 moveId, moveSlot; + s32 target; + + INVALID_IF(DATA.turnState == TURN_CLOSED, "MOVE outside TURN"); + INVALID_IF(IsAITest() && (battlerId & BIT_SIDE) == B_SIDE_OPPONENT, "MOVE is not allowed for opponent in AI tests. Use EXPECT_MOVE instead"); + + MoveGetIdAndSlot(battlerId, &ctx, &moveId, &moveSlot, sourceLine); + target = MoveGetTarget(battlerId, moveId, &ctx, sourceLine); if (ctx.explicitHit) DATA.battleRecordTurns[DATA.turns][battlerId].hit = 1 + ctx.hit; @@ -1672,6 +2046,153 @@ void ForcedMove(u32 sourceLine, struct BattlePokemon *battler) } } +static void TryMarkExpectMove(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext *ctx) +{ + s32 battlerId = battler - gBattleMons; + u32 moveId, moveSlot, id; + s32 target; + + INVALID_IF(DATA.turnState == TURN_CLOSED, "EXPECT_MOVE outside TURN"); + INVALID_IF(!IsAITest(), "EXPECT_MOVE is usable only in AI_SINGLE_BATTLE_TEST & AI_DOUBLE_BATTLE_TEST"); + MoveGetIdAndSlot(battlerId, ctx, &moveId, &moveSlot, sourceLine); + target = MoveGetTarget(battlerId, moveId, ctx, sourceLine); + + id = DATA.expectedAiActionIndex[battlerId]; + DATA.expectedAiActions[battlerId][id].type = B_ACTION_USE_MOVE; + DATA.expectedAiActions[battlerId][id].moveSlots |= gBitTable[moveSlot]; + DATA.expectedAiActions[battlerId][id].target = target; + DATA.expectedAiActions[battlerId][id].explicitTarget = ctx->explicitTarget; + DATA.expectedAiActions[battlerId][id].sourceLine = sourceLine; + DATA.expectedAiActions[battlerId][id].actionSet = TRUE; + if (ctx->explicitNotExpected) + DATA.expectedAiActions[battlerId][id].notMove = ctx->notExpected; + + DATA.actionBattlers |= 1 << battlerId; + DATA.moveBattlers |= 1 << battlerId; +} + +void ExpectMove(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx) +{ + s32 battlerId = battler - gBattleMons; + TryMarkExpectMove(sourceLine, battler, &ctx); + DATA.expectedAiActionIndex[battlerId]++; +} + +void ExpectSendOut(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex) +{ + s32 i, id; + s32 battlerId = battler - gBattleMons; + INVALID_IF(DATA.turnState == TURN_CLOSED, "EXPECT_SEND_OUT outside TURN"); + INVALID_IF(!IsAITest(), "EXPECT_SEND_OUT is usable only in AI_SINGLE_BATTLE_TEST & AI_DOUBLE_BATTLE_TEST"); + INVALID_IF(partyIndex >= ((battlerId & BIT_SIDE) == B_SIDE_PLAYER ? DATA.playerPartySize : DATA.opponentPartySize), "EXPECT_SEND_OUT to invalid party index"); + for (i = 0; i < STATE->battlersCount; i++) + { + if (battlerId != i && (battlerId & BIT_SIDE) == (i & BIT_SIDE)) + INVALID_IF(DATA.currentMonIndexes[i] == partyIndex, "EXPECT_SEND_OUT to battler"); + } + if (!(DATA.actionBattlers & (1 << battlerId))) + { + const struct BattleTest *test = GetBattleTest(); + if (IsAITest() && (battlerId & BIT_SIDE) == B_SIDE_OPPONENT) // If Move was not specified, allow any move used. + SetAiActionToPass(sourceLine, battlerId); + else + Move(sourceLine, battler, (struct MoveContext) { move: MOVE_CELEBRATE, explicitMove: TRUE }); + } + + DATA.currentMonIndexes[battlerId] = partyIndex; + DATA.actionBattlers |= 1 << battlerId; + + id = DATA.expectedAiActionIndex[battlerId]; + DATA.expectedAiActions[battlerId][id].type = B_ACTION_SWITCH; + DATA.expectedAiActions[battlerId][id].target = partyIndex; + DATA.expectedAiActions[battlerId][id].sourceLine = sourceLine; + DATA.expectedAiActions[battlerId][id].actionSet = TRUE; + DATA.expectedAiActionIndex[battlerId]++; +} + +s32 GetAiMoveTargetForScoreCompare(u32 battlerId, u32 moveId, struct MoveContext *ctx, u32 sourceLine) +{ + s32 target; + + // In Single Battles ai always targets the opposing mon. + if (GetBattleTest()->type == BATTLE_TEST_AI_SINGLES) + { + target = BATTLE_OPPOSITE(battlerId); + } + else + { + // TODO: Fix ai targeting self in double battles. + INVALID_IF(!ctx->explicitTarget, "%S requires explicit target for score comparison in doubles", gMoveNames[moveId]); + target = MoveGetTarget(battlerId, moveId, ctx, sourceLine); + } + return target; +} + +void Score(u32 sourceLine, struct BattlePokemon *battler, u32 cmp, bool32 toValue, struct TestAIScoreStruct cmpCtx) +{ + u32 moveSlot1, moveSlot2; + s32 i, target; + struct MoveContext moveCtx = {0}; + s32 battlerId = battler - gBattleMons; + s32 turn = DATA.turns; + + INVALID_IF(!IsAITest(), "SCORE_%s%s is usable only in AI_SINGLE_BATTLE_TEST & AI_DOUBLE_BATTLE_TEST", sCmpToStringTable[cmp], (toValue == TRUE) ? "_VAL" : ""); + + for (i = 0; i < MAX_AI_SCORE_COMPARISION_PER_TURN; i++) + { + if (!DATA.expectedAiScores[battlerId][turn][i].set) + break; + } + + INVALID_IF(i == MAX_AI_SCORE_COMPARISION_PER_TURN, "Too many EXPECTs in TURN"); + + moveCtx.move = cmpCtx.move1; + moveCtx.explicitMove = cmpCtx.explicitMove1; + moveCtx.target = cmpCtx.target; + moveCtx.explicitTarget = cmpCtx.explicitTarget; + MoveGetIdAndSlot(battlerId, &moveCtx, &cmpCtx.move1, &moveSlot1, sourceLine); + // For ai moves, target is never self. + target = GetAiMoveTargetForScoreCompare(battlerId, cmpCtx.move1, &moveCtx, sourceLine); + DATA.expectedAiScores[battlerId][turn][i].target = target; + DATA.expectedAiScores[battlerId][turn][i].moveSlot1 = moveSlot1; + DATA.expectedAiScores[battlerId][turn][i].cmp = cmp; + DATA.expectedAiScores[battlerId][turn][i].toValue = toValue; + if (toValue) + { + DATA.expectedAiScores[battlerId][turn][i].value = cmpCtx.valueOrMoveId2; + } + else + { + moveCtx.move = cmpCtx.valueOrMoveId2; + moveCtx.explicitMove = cmpCtx.explicitValueOrMoveId2; + moveCtx.target = cmpCtx.target; + moveCtx.explicitTarget = cmpCtx.explicitTarget; + MoveGetIdAndSlot(battlerId, &moveCtx, &cmpCtx.valueOrMoveId2, &moveSlot2, sourceLine); + DATA.expectedAiScores[battlerId][turn][i].moveSlot2 = moveSlot2; + } + DATA.expectedAiScores[battlerId][turn][i].sourceLine = sourceLine; + DATA.expectedAiScores[battlerId][turn][i].set = TRUE; +} + +void ExpectMoves(u32 sourceLine, struct BattlePokemon *battler, bool32 notExpected, struct FourMoves moves) +{ + s32 battlerId = battler - gBattleMons; + u32 i; + + for (i = 0; i < MAX_BATTLERS_COUNT; i++) + { + if (moves.moves[i] != MOVE_NONE) + { + struct MoveContext ctx = {0}; + ctx.move = moves.moves[i]; + ctx.explicitMove = ctx.explicitNotExpected = TRUE; + ctx.notExpected = notExpected; + TryMarkExpectMove(sourceLine, battler, &ctx); + } + } + DATA.expectedAiActionIndex[battlerId]++; +} + void Switch(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex) { s32 i; @@ -1679,6 +2200,7 @@ void Switch(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex) INVALID_IF(DATA.turnState == TURN_CLOSED, "SWITCH outside TURN"); INVALID_IF(DATA.actionBattlers & (1 << battlerId), "Multiple battler actions"); INVALID_IF(partyIndex >= ((battlerId & BIT_SIDE) == B_SIDE_PLAYER ? DATA.playerPartySize : DATA.opponentPartySize), "SWITCH to invalid party index"); + INVALID_IF(IsAITest() && (battlerId & BIT_SIDE) == B_SIDE_OPPONENT, "SWITCH is not allowed for opponent in AI tests. Use EXPECT_SWITCH instead"); for (i = 0; i < STATE->battlersCount; i++) { @@ -1693,6 +2215,32 @@ void Switch(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex) DATA.actionBattlers |= 1 << battlerId; } +void ExpectSwitch(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex) +{ + s32 i, id; + s32 battlerId = battler - gBattleMons; + INVALID_IF(DATA.turnState == TURN_CLOSED, "EXPECT_SWITCH outside TURN"); + INVALID_IF(!IsAITest(), "EXPECT_SWITCH is usable only in AI_SINGLE_BATTLE_TEST & AI_DOUBLE_BATTLE_TEST"); + INVALID_IF(DATA.actionBattlers & (1 << battlerId), "Multiple battler actions"); + INVALID_IF(partyIndex >= ((battlerId & BIT_SIDE) == B_SIDE_PLAYER ? DATA.playerPartySize : DATA.opponentPartySize), "EXPECT_SWITCH to invalid party index"); + + for (i = 0; i < STATE->battlersCount; i++) + { + if (battlerId != i && (battlerId & BIT_SIDE) == (i & BIT_SIDE)) + INVALID_IF(DATA.currentMonIndexes[i] == partyIndex, "EXPECT_SWITCH to battler"); + } + + DATA.currentMonIndexes[battlerId] = partyIndex; + DATA.actionBattlers |= 1 << battlerId; + + id = DATA.expectedAiActionIndex[battlerId]; + DATA.expectedAiActions[battlerId][id].type = B_ACTION_SWITCH; + DATA.expectedAiActions[battlerId][id].target = partyIndex; + DATA.expectedAiActions[battlerId][id].sourceLine = sourceLine; + DATA.expectedAiActions[battlerId][id].actionSet = TRUE; + DATA.expectedAiActionIndex[battlerId]++; +} + void SkipTurn(u32 sourceLine, struct BattlePokemon *battler) { s32 battlerId = battler - gBattleMons; @@ -1706,6 +2254,7 @@ void SendOut(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex) s32 battlerId = battler - gBattleMons; INVALID_IF(DATA.turnState == TURN_CLOSED, "SEND_OUT outside TURN"); INVALID_IF(partyIndex >= ((battlerId & BIT_SIDE) == B_SIDE_PLAYER ? DATA.playerPartySize : DATA.opponentPartySize), "SWITCH to invalid party index"); + INVALID_IF(IsAITest() && (battlerId & BIT_SIDE) == B_SIDE_OPPONENT, "SEND_OUT is not allowed for opponent in AI tests. Use EXPECT_SEND_OUT instead"); for (i = 0; i < STATE->battlersCount; i++) { if (battlerId != i && (battlerId & BIT_SIDE) == (i & BIT_SIDE)) @@ -1925,7 +2474,6 @@ void QueueExp(u32 sourceLine, struct BattlePokemon *battler, struct ExpEventCont }; } - void QueueMessage(u32 sourceLine, const u8 *pattern) { INVALID_IF(!STATE->runScene, "MESSAGE outside of SCENE"); @@ -1994,3 +2542,45 @@ u32 TestRunner_Battle_GetForcedAbility(u32 side, u32 partyIndex) { return DATA.forcedAbilities[side][partyIndex]; } + +// TODO: Consider storing the last successful i and searching from i+1 +// to improve performance. +struct AILogLine *GetLogLine(u32 battlerId, u32 moveIndex) +{ + s32 i, j; + + for (i = 0; i < MAX_AI_LOG_LINES; i++) + { + struct AILogLine *log = &DATA.aiLogLines[battlerId][moveIndex][i]; + if (log->file == NULL) + { + return log; + } + } + + Test_ExitWithResult(TEST_RESULT_ERROR, "Too many AI log lines"); +} + +void TestRunner_Battle_AILogScore(const char *file, u32 line, u32 battlerId, u32 moveIndex, s32 score, bool32 setScore) +{ + s32 i; + struct AILogLine *log; + + if (!DATA.logAI) return; + + log = GetLogLine(battlerId, moveIndex); + log->file = file; + log->line = line; + log->score = score; + log->set = setScore; +} + +void TestRunner_Battle_AISetScore(const char *file, u32 line, u32 battlerId, u32 moveIndex, s32 score) +{ + TestRunner_Battle_AILogScore(file, line, battlerId, moveIndex, score, TRUE); +} + +void TestRunner_Battle_AIAdjustScore(const char *file, u32 line, u32 battlerId, u32 moveIndex, s32 score) +{ + TestRunner_Battle_AILogScore(file, line, battlerId, moveIndex, score, FALSE); +}