From 297cd83871890e040a7f37d4cfb16518bdbafae0 Mon Sep 17 00:00:00 2001 From: hedara90 <90hedara@gmail.com> Date: Sat, 2 Aug 2025 17:43:44 +0200 Subject: [PATCH 01/26] Clean up some Normalize tests to make them make sense (#7461) Co-authored-by: Hedara --- test/battle/ability/normalize.c | 40 +++++++-------------------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/test/battle/ability/normalize.c b/test/battle/ability/normalize.c index 2a449f74a5..33fe89c587 100644 --- a/test/battle/ability/normalize.c +++ b/test/battle/ability/normalize.c @@ -156,53 +156,29 @@ SINGLE_BATTLE_TEST("Normalize boosts power of affected moves by 20% (Gen7+)", s1 } } -SINGLE_BATTLE_TEST("Normalize-affected moves become Electric-type under Electrify's effect", s16 damage) +SINGLE_BATTLE_TEST("Normalize-affected moves become Electric-type under Electrify's effect") { - u32 ability, genConfig; - PARAMETRIZE { ability = ABILITY_CUTE_CHARM; genConfig = GEN_7; } - PARAMETRIZE { ability = ABILITY_CUTE_CHARM; genConfig = GEN_6; } - PARAMETRIZE { ability = ABILITY_NORMALIZE; genConfig = GEN_7; } - PARAMETRIZE { ability = ABILITY_NORMALIZE; genConfig = GEN_6; } - GIVEN { ASSUME(GetMoveEffect(MOVE_ELECTRIFY) == EFFECT_ELECTRIFY); - WITH_CONFIG(GEN_CONFIG_ATE_MULTIPLIER, genConfig); - PLAYER(SPECIES_SKITTY) { Ability(ability); Moves(MOVE_WATER_GUN); } - OPPONENT(SPECIES_WOBBUFFET); + PLAYER(SPECIES_SKITTY) { Ability(ABILITY_NORMALIZE); } + OPPONENT(SPECIES_ROOKIDEE) { Item(ITEM_WACAN_BERRY); } } WHEN { TURN { MOVE(opponent, MOVE_ELECTRIFY); MOVE(player, MOVE_WATER_GUN); } } SCENE { - HP_BAR(opponent, captureDamage: &results[i].damage); - } FINALLY { - if (genConfig >= GEN_7) - EXPECT_EQ(results[0].damage, results[2].damage); - else - EXPECT_EQ(results[1].damage, results[3].damage); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent); } } -SINGLE_BATTLE_TEST("Normalize-affected moves become Electric-type under Ion Deluge's effect", s16 damage) +SINGLE_BATTLE_TEST("Normalize-affected moves become Electric-type under Ion Deluge's effect") { - u32 ability, genConfig; - PARAMETRIZE { ability = ABILITY_CUTE_CHARM; genConfig = GEN_7; } - PARAMETRIZE { ability = ABILITY_CUTE_CHARM; genConfig = GEN_6; } - PARAMETRIZE { ability = ABILITY_NORMALIZE; genConfig = GEN_7; } - PARAMETRIZE { ability = ABILITY_NORMALIZE; genConfig = GEN_6; } - GIVEN { ASSUME(GetMoveEffect(MOVE_ION_DELUGE) == EFFECT_ION_DELUGE); - WITH_CONFIG(GEN_CONFIG_ATE_MULTIPLIER, genConfig); - PLAYER(SPECIES_SKITTY) { Ability(ability); Moves(MOVE_WATER_GUN); } - OPPONENT(SPECIES_WOBBUFFET); + PLAYER(SPECIES_SKITTY) { Ability(ABILITY_NORMALIZE); Moves(MOVE_WATER_GUN); } + OPPONENT(SPECIES_ROOKIDEE) { Item(ITEM_WACAN_BERRY); } } WHEN { TURN { MOVE(opponent, MOVE_ION_DELUGE); MOVE(player, MOVE_WATER_GUN); } } SCENE { - HP_BAR(opponent, captureDamage: &results[i].damage); - } FINALLY { - if (genConfig >= GEN_7) - EXPECT_EQ(results[0].damage, results[2].damage); - else - EXPECT_EQ(results[1].damage, results[3].damage); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent); } } From 54ffc6bf0856661be1289afa7961d30e1a2c3414 Mon Sep 17 00:00:00 2001 From: AlexOn1ine Date: Sun, 3 Aug 2025 21:59:48 +0200 Subject: [PATCH 02/26] Fixes Leech Seed recovery not applying behind Sub --- data/battle_scripts_1.s | 1 + test/battle/move_effect/leech_seed.c | 28 +++++++++++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 13d3c09012..23c1e10aa9 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -5776,6 +5776,7 @@ BattleScript_LeechSeedTurnDrainHealBlock:: BattleScript_LeechSeedTurnDrainRecovery:: call BattleScript_LeechSeedTurnDrain BattleScript_LeechSeedTurnDrainGainHp: + orword gHitMarker, HITMARKER_IGNORE_SUBSTITUTE | HITMARKER_PASSIVE_DAMAGE healthbarupdate BS_TARGET datahpupdate BS_TARGET printfromtable gLeechSeedStringIds diff --git a/test/battle/move_effect/leech_seed.c b/test/battle/move_effect/leech_seed.c index 56da50a12b..49f963f416 100644 --- a/test/battle/move_effect/leech_seed.c +++ b/test/battle/move_effect/leech_seed.c @@ -66,11 +66,11 @@ DOUBLE_BATTLE_TEST("Leech Seed will drain HP based on speed of the drained mon") OPPONENT(SPECIES_WYNAUT) { Speed(3); } OPPONENT(SPECIES_WOBBUFFET) { Speed(4); } } WHEN { - TURN { - MOVE(playerLeft, MOVE_LEECH_SEED, target: opponentLeft); - MOVE(playerRight, MOVE_LEECH_SEED, target: opponentRight); - MOVE(opponentLeft, MOVE_LEECH_SEED, target: playerLeft); - MOVE(opponentRight, MOVE_LEECH_SEED, target: playerRight); + TURN { + MOVE(playerLeft, MOVE_LEECH_SEED, target: opponentLeft); + MOVE(playerRight, MOVE_LEECH_SEED, target: opponentRight); + MOVE(opponentLeft, MOVE_LEECH_SEED, target: playerLeft); + MOVE(opponentRight, MOVE_LEECH_SEED, target: playerRight); } } SCENE { ANIMATION(ANIM_TYPE_MOVE, MOVE_LEECH_SEED, opponentRight); @@ -88,6 +88,24 @@ DOUBLE_BATTLE_TEST("Leech Seed will drain HP based on speed of the drained mon") } } +SINGLE_BATTLE_TEST("Leech Seeded recovers health through Substitute") +{ + GIVEN { + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_SUBSTITUTE); } + TURN { MOVE(player, MOVE_LEECH_SEED); } + TURN {} + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SUBSTITUTE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_LEECH_SEED, player); + HP_BAR(player); + HP_BAR(opponent); + HP_BAR(player); + } +} + TO_DO_BATTLE_TEST("Leech Seed doesn't affect already seeded targets") TO_DO_BATTLE_TEST("Leech Seed's effect is paused until a new battler replaces the original user's position") // Faint, can't be replaced, then revived. TO_DO_BATTLE_TEST("Leech Seed's effect pause still prevents it from being seeded again") From 014f289ea9e279d5f2008b0c8ebecdfc24d84c70 Mon Sep 17 00:00:00 2001 From: PhallenTree <168426989+PhallenTree@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:41:43 +0100 Subject: [PATCH 03/26] Fixes Round and Fusion moves doubling power from previous turn's move (#7476) --- src/battle_script_commands.c | 7 ++++--- src/battle_util.c | 22 ++++++++++------------ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 6798b9bd9c..d749d450d7 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -6829,6 +6829,7 @@ static void Cmd_moveend(void) UpdateStallMons(); if ((gBattleStruct->moveResultFlags[gBattlerTarget] & (MOVE_RESULT_FAILED | MOVE_RESULT_DOESNT_AFFECT_FOE)) || (gBattleMons[gBattlerAttacker].status2 & (STATUS2_FLINCHED)) + || gBattleStruct->pledgeMove == TRUE // Is the battler that uses the first Pledge move in the combo || gProtectStructs[gBattlerAttacker].nonVolatileStatusImmobility) gBattleStruct->battlerState[gBattlerAttacker].stompingTantrumTimer = 2; @@ -12194,7 +12195,7 @@ void BS_RemoveStockpileCounters(void) { NATIVE_ARGS(); - if (GetMoveEffect(gCurrentMove) == EFFECT_SWALLOW + if (GetMoveEffect(gCurrentMove) == EFFECT_SPIT_UP && gSpecialStatuses[gBattlerAttacker].parentalBondState == PARENTAL_BOND_1ST_HIT && IsBattlerAlive(gBattlerTarget)) { @@ -16996,7 +16997,7 @@ static void TryUpdateRoundTurnOrder(void) } // update turn order for round users - for (i = 0; roundUsers[i] != 0xFF && i < 3; i++) + for (i = 0; i < 3 && roundUsers[i] != 0xFF; i++) { gBattlerByTurnOrder[currRounder] = roundUsers[i]; gProtectStructs[roundUsers[i]].quash = TRUE; // Make it so their turn order can't be changed again @@ -17004,7 +17005,7 @@ static void TryUpdateRoundTurnOrder(void) } // Update turn order for non-round users - for (i = 0; nonRoundUsers[i] != 0xFF && i < 3; i++) + for (i = 0; i < 3 && nonRoundUsers[i] != 0xFF; i++) { gBattlerByTurnOrder[currRounder] = nonRoundUsers[i]; currRounder++; diff --git a/src/battle_util.c b/src/battle_util.c index e580c63616..fb28a6129c 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -8072,6 +8072,7 @@ static inline u32 CalcMoveBasePower(struct DamageCalculationData *damageCalcData u32 battlerAtk = damageCalcData->battlerAtk; u32 battlerDef = damageCalcData->battlerDef; u32 move = damageCalcData->move; + u32 moveEffect = GetMoveEffect(move); u32 i; u32 basePower = GetMovePower(move); @@ -8083,7 +8084,7 @@ static inline u32 CalcMoveBasePower(struct DamageCalculationData *damageCalcData if (GetActiveGimmick(battlerAtk) == GIMMICK_DYNAMAX) return GetMaxMovePower(move); - switch (GetMoveEffect(move)) + switch (moveEffect) { case EFFECT_PLEDGE: if (gBattleStruct->pledgeMove) @@ -8242,18 +8243,15 @@ static inline u32 CalcMoveBasePower(struct DamageCalculationData *damageCalcData || gDisableStructs[battlerDef].isFirstTurn == 2) basePower *= 2; break; - case EFFECT_ROUND: - for (i = 0; i < gBattlersCount; i++) - { - if (i != battlerAtk && IsBattlerAlive(i) && GetMoveEffect(gLastUsedMove) == EFFECT_ROUND) - { - basePower *= 2; - break; - } - } - break; case EFFECT_FUSION_COMBO: - if (GetMoveEffect(gLastUsedMove) == EFFECT_FUSION_COMBO && move != gLastUsedMove) + if (move == gLastUsedMove) + break; + // fallthrough + case EFFECT_ROUND: + // don't double power due to previous turn's Round/Fusion move + if (gCurrentTurnActionNumber != 0 + && gActionsByTurnOrder[gCurrentTurnActionNumber - 1] == B_ACTION_USE_MOVE + && GetMoveEffect(gLastUsedMove) == moveEffect) basePower *= 2; break; case EFFECT_LASH_OUT: From 3dbcb2adb4a11ccec6b8a6547a401d52273d257a Mon Sep 17 00:00:00 2001 From: ghoulslash <41651341+ghoulslash@users.noreply.github.com> Date: Mon, 4 Aug 2025 06:42:22 -0400 Subject: [PATCH 04/26] Add Synthesis + Utililty Umbrella test (#7472) Co-authored-by: ghoulslash --- test/battle/move_effect/synthesis.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/battle/move_effect/synthesis.c b/test/battle/move_effect/synthesis.c index 6799bd2870..954cf2e854 100644 --- a/test/battle/move_effect/synthesis.c +++ b/test/battle/move_effect/synthesis.c @@ -46,3 +46,21 @@ SINGLE_BATTLE_TEST("Synthesis recovers 1/4 of the user's max HP in Rain, Sandsto HP_BAR(player, damage: -(400 / 4)); } } + +SINGLE_BATTLE_TEST("Synthesis recovers regular amount in sandstorm if holding utility umbrella") +{ + u32 item; + PARAMETRIZE { item = ITEM_LIFE_ORB; } + PARAMETRIZE { item = ITEM_UTILITY_UMBRELLA; } + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { HP(1); MaxHP(400); Item(item); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_SANDSTORM); MOVE(player, MOVE_SYNTHESIS); } + } SCENE { + if (item != ITEM_UTILITY_UMBRELLA) + HP_BAR(player, damage: -(400 / 4)); + else + HP_BAR(player, damage: -(400 / 2)); + } +} From 523390ac5151b4a7bc70bc77baf901f10ee0e09e Mon Sep 17 00:00:00 2001 From: Kevin <85093953+kevinwklawrence@users.noreply.github.com> Date: Mon, 4 Aug 2025 07:49:05 -0400 Subject: [PATCH 05/26] Update how_to_new_pokemon.md (#7440) --- docs/tutorials/how_to_new_pokemon.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/how_to_new_pokemon.md b/docs/tutorials/how_to_new_pokemon.md index de2d41d57c..37c0d28ae3 100644 --- a/docs/tutorials/how_to_new_pokemon.md +++ b/docs/tutorials/how_to_new_pokemon.md @@ -524,9 +524,9 @@ Edit [src/data/graphics/pokemon.h](https://github.com/rh-hideout/pokeemerald-exp ```diff #if P_FAMILY_PECHARUNT const u32 gMonFrontPic_Pecharunt[] = INCBIN_U32("graphics/pokemon/pecharunt/front.4bpp.lz"); - const u32 gMonPalette_Pecharunt[] = INCBIN_U32("graphics/pokemon/pecharunt/normal.gbapal.lz"); + const u16 gMonPalette_Pecharunt[] = INCBIN_U16("graphics/pokemon/pecharunt/normal.gbapal"); const u32 gMonBackPic_Pecharunt[] = INCBIN_U32("graphics/pokemon/pecharunt/back.4bpp.lz"); - const u32 gMonShinyPalette_Pecharunt[] = INCBIN_U32("graphics/pokemon/pecharunt/shiny.gbapal.lz"); + const u16 gMonShinyPalette_Pecharunt[] = INCBIN_U16("graphics/pokemon/pecharunt/shiny.gbapal"); const u8 gMonIcon_Pecharunt[] = INCBIN_U8("graphics/pokemon/pecharunt/icon.4bpp"); #if P_FOOTPRINTS const u8 gMonFootprint_Pecharunt[] = INCBIN_U8("graphics/pokemon/pecharunt/footprint.1bpp"); @@ -534,20 +534,20 @@ Edit [src/data/graphics/pokemon.h](https://github.com/rh-hideout/pokeemerald-exp #if OW_POKEMON_OBJECT_EVENTS const u32 gObjectEventPic_Pecharunt[] = INCBIN_COMP("graphics/pokemon/pecharunt/overworld.4bpp"); #if OW_PKMN_OBJECTS_SHARE_PALETTES == FALSE - const u32 gOverworldPalette_Pecharunt[] = INCBIN_U32("graphics/pokemon/pecharunt/overworld_normal.gbapal.lz"); - const u32 gShinyOverworldPalette_Pecharunt[] = INCBIN_U32("graphics/pokemon/pecharunt/overworld_shiny.gbapal.lz"); + const u16 gOverworldPalette_Pecharunt[] = INCBIN_U16("graphics/pokemon/pecharunt/overworld_normal.gbapal"); + const u16 gShinyOverworldPalette_Pecharunt[] = INCBIN_U16("graphics/pokemon/pecharunt/overworld_shiny.gbapal"); #endif //OW_PKMN_OBJECTS_SHARE_PALETTES #endif //OW_POKEMON_OBJECT_EVENTS #endif //P_FAMILY_PECHARUNT const u32 gMonFrontPic_Egg[] = INCBIN_U32("graphics/pokemon/egg/anim_front.4bpp.lz"); - const u32 gMonPalette_Egg[] = INCBIN_U32("graphics/pokemon/egg/normal.gbapal.lz"); + const u16 gMonPalette_Egg[] = INCBIN_U16("graphics/pokemon/egg/normal.gbapal"); const u8 gMonIcon_Egg[] = INCBIN_U8("graphics/pokemon/egg/icon.4bpp"); + const u32 gMonFrontPic_Mewthree[] = INCBIN_U32("graphics/pokemon/mewthree/anim_front.4bpp.lz"); + const u32 gMonBackPic_Mewthree[] = INCBIN_U32("graphics/pokemon/mewthree/back.4bpp.lz"); -+ const u32 gMonPalette_Mewthree[] = INCBIN_U32("graphics/pokemon/mewthree/normal.gbapal.lz"); -+ const u32 gMonShinyPalette_Mewthree[] = INCBIN_U32("graphics/pokemon/mewthree/shiny.gbapal.lz"); ++ const u16 gMonPalette_Mewthree[] = INCBIN_U16("graphics/pokemon/mewthree/normal.gbapal"); ++ const u16 gMonShinyPalette_Mewthree[] = INCBIN_U16("graphics/pokemon/mewthree/shiny.gbapal"); + const u8 gMonIcon_Mewthree[] = INCBIN_U8("graphics/pokemon/mewthree/icon.4bpp"); + const u8 gMonFootprint_Mewthree[] = INCBIN_U8("graphics/pokemon/mewthree/footprint.1bpp"); ``` From c037ba252240e693dcfb27c5ca09b7c0545f0436 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Mon, 4 Aug 2025 14:44:58 +0200 Subject: [PATCH 06/26] Clear gScanlineEffect to fix timeout in acid downpour animation (#7442) --- src/battle_main.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/battle_main.c b/src/battle_main.c index 8bab8fd1ef..a5dd01485d 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -487,10 +487,9 @@ static void CB2_InitBattleInternal(void) else { gBattle_WIN0V = WIN_RANGE(DISPLAY_HEIGHT / 2, DISPLAY_HEIGHT / 2 + 1); + ScanlineEffect_Clear(); if (B_FAST_INTRO_NO_SLIDE == FALSE && !gTestRunnerHeadless) { - ScanlineEffect_Clear(); - for (i = 0; i < DISPLAY_HEIGHT / 2; i++) { gScanlineEffectRegBuffers[0][i] = 0xF0; From f6a120310e69be91646201ab2121e3847f076797 Mon Sep 17 00:00:00 2001 From: Bivurnum <147376167+Bivurnum@users.noreply.github.com> Date: Mon, 4 Aug 2025 08:10:20 -0500 Subject: [PATCH 07/26] Bug fix: add handling for forced movements into/after ledge jumps with follower NPCs (#7055) --- include/follower_npc.h | 7 ++++-- include/global.h | 3 ++- src/field_player_avatar.c | 23 +++++++++++++----- src/follower_npc.c | 49 +++++++++++---------------------------- 4 files changed, 38 insertions(+), 44 deletions(-) diff --git a/include/follower_npc.h b/include/follower_npc.h index 52355a78b7..abfbf0962a 100644 --- a/include/follower_npc.h +++ b/include/follower_npc.h @@ -20,6 +20,7 @@ enum FollowerNPCDataTypes FNPC_DATA_WARP_END, FNPC_DATA_SURF_BLOB, FNPC_DATA_COME_OUT_DOOR, + FNPC_DATA_FORCED_MOVEMENT, FNPC_DATA_OBJ_ID, FNPC_DATA_CURRENT_SPRITE, FNPC_DATA_DELAYED_STATE, @@ -62,7 +63,8 @@ enum FollowerNPCSurfBlobStates FNPC_SURF_BLOB_DESTROY }; -enum FollowerNPCOutOfDoorTaskStates{ +enum FollowerNPCOutOfDoorTaskStates +{ OPEN_DOOR, NPC_WALK_OUT, CLOSE_DOOR, @@ -70,7 +72,8 @@ enum FollowerNPCOutOfDoorTaskStates{ REALLOW_MOVEMENT }; -enum FollowerNPCHandleEscalatorFinishTaskStates{ +enum FollowerNPCHandleEscalatorFinishTaskStates +{ MOVE_TO_PLAYER_POS, WAIT_FOR_PLAYER_MOVE, SHOW_FOLLOWER_DOWN, diff --git a/include/global.h b/include/global.h index f4c944b64f..27c0a85e15 100644 --- a/include/global.h +++ b/include/global.h @@ -219,7 +219,8 @@ struct NPCFollower u8 inProgress:1; u8 warpEnd:1; u8 createSurfBlob:3; - u8 comeOutDoorStairs:3; + u8 comeOutDoorStairs:2; + u8 forcedMovement:1; u8 objId; u8 currentSprite; u8 delayedState; diff --git a/src/field_player_avatar.c b/src/field_player_avatar.c index d02ecbd9fc..55542a3588 100644 --- a/src/field_player_avatar.c +++ b/src/field_player_avatar.c @@ -382,8 +382,16 @@ void PlayerStep(u8 direction, u16 newKeys, u16 heldKeys) DoPlayerAvatarTransition(); if (TryDoMetatileBehaviorForcedMovement() == 0) { - MovePlayerAvatarUsingKeypadInput(direction, newKeys, heldKeys); - PlayerAllowForcedMovementIfMovingSameDirection(); + if (GetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT) != FALSE) + { + gPlayerAvatar.preventStep = TRUE; + CreateTask(Task_MoveNPCFollowerAfterForcedMovement, 1); + } + else + { + MovePlayerAvatarUsingKeypadInput(direction, newKeys, heldKeys); + PlayerAllowForcedMovementIfMovingSameDirection(); + } } } } @@ -510,7 +518,11 @@ static bool8 DoForcedMovement(u8 direction, void (*moveFunc)(u8)) else { if (collision == COLLISION_LEDGE_JUMP) + { + SetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT, FALSE); PlayerJumpLedge(direction); + } + playerAvatar->flags |= PLAYER_AVATAR_FLAG_FORCED_MOVE; playerAvatar->runningState = MOVING; return TRUE; @@ -518,12 +530,11 @@ static bool8 DoForcedMovement(u8 direction, void (*moveFunc)(u8)) } else { + if (PlayerHasFollowerNPC()) + SetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT, TRUE); + playerAvatar->runningState = MOVING; moveFunc(direction); - if (PlayerHasFollowerNPC() - && gObjectEvents[GetFollowerNPCObjectId()].invisible == FALSE - && FindTaskIdByFunc(Task_MoveNPCFollowerAfterForcedMovement) == TASK_NONE) - CreateTask(Task_MoveNPCFollowerAfterForcedMovement, 3); return TRUE; } } diff --git a/src/follower_npc.c b/src/follower_npc.c index d9882b61bf..fcfe60dda8 100644 --- a/src/follower_npc.c +++ b/src/follower_npc.c @@ -83,6 +83,9 @@ void SetFollowerNPCData(enum FollowerNPCDataTypes type, u32 value) case FNPC_DATA_COME_OUT_DOOR: gSaveBlock3Ptr->NPCfollower.comeOutDoorStairs = value; break; + case FNPC_DATA_FORCED_MOVEMENT: + gSaveBlock3Ptr->NPCfollower.forcedMovement = value; + break; case FNPC_DATA_OBJ_ID: gSaveBlock3Ptr->NPCfollower.objId = value; break; @@ -155,6 +158,8 @@ u32 GetFollowerNPCData(enum FollowerNPCDataTypes type) return gSaveBlock3Ptr->NPCfollower.createSurfBlob; case FNPC_DATA_COME_OUT_DOOR: return gSaveBlock3Ptr->NPCfollower.comeOutDoorStairs; + case FNPC_DATA_FORCED_MOVEMENT: + return gSaveBlock3Ptr->NPCfollower.forcedMovement; case FNPC_DATA_OBJ_ID: return gSaveBlock3Ptr->NPCfollower.objId; case FNPC_DATA_CURRENT_SPRITE: @@ -720,9 +725,7 @@ u32 DetermineFollowerNPCState(struct ObjectEvent *follower, u32 state, u32 direc MoveCoords(direction, &followerX, &followerY); nextBehavior = MapGridGetMetatileBehaviorAt(followerX, followerY); - - if (FindTaskIdByFunc(Task_MoveNPCFollowerAfterForcedMovement) == TASK_NONE) - follower->facingDirectionLocked = FALSE; + follower->facingDirectionLocked = FALSE; // Follower won't do delayed movement until player does a movement. if (!IsStateMovement(state) && delayedState) @@ -777,10 +780,6 @@ u32 DetermineFollowerNPCState(struct ObjectEvent *follower, u32 state, u32 direc RETURN_STATE(MOVEMENT_ACTION_WALK_NORMAL_DOWN, direction); case MOVEMENT_ACTION_WALK_FAST_DOWN ... MOVEMENT_ACTION_WALK_FAST_RIGHT: - // Handle player on waterfall. - if (PlayerIsUnderWaterfall(&gObjectEvents[gPlayerAvatar.objectEventId]) && (state == MOVEMENT_ACTION_WALK_FAST_UP)) - return MOVEMENT_INVALID; - // Handle ice tile (some walking animation). if (MetatileBehavior_IsIce(follower->currentMetatileBehavior) || MetatileBehavior_IsTrickHouseSlipperyFloor(follower->currentMetatileBehavior)) follower->disableAnim = TRUE; @@ -789,6 +788,9 @@ u32 DetermineFollowerNPCState(struct ObjectEvent *follower, u32 state, u32 direc if (GetFollowerNPCData(FNPC_DATA_CURRENT_SPRITE) == FOLLOWER_NPC_SPRITE_INDEX_SURF && GetFollowerNPCSprite() == GetFollowerNPCData(FNPC_DATA_GFX_ID)) RETURN_STATE(MOVEMENT_ACTION_SURF_STILL_DOWN, direction); + if (MetatileBehavior_IsMuddySlope(follower->currentMetatileBehavior)) + follower->facingDirectionLocked = TRUE; + RETURN_STATE(MOVEMENT_ACTION_WALK_FAST_DOWN, direction); case MOVEMENT_ACTION_WALK_FASTER_DOWN ... MOVEMENT_ACTION_WALK_FASTER_RIGHT: @@ -798,10 +800,6 @@ u32 DetermineFollowerNPCState(struct ObjectEvent *follower, u32 state, u32 direc RETURN_STATE(MOVEMENT_ACTION_WALK_FASTER_DOWN, direction); case MOVEMENT_ACTION_RIDE_WATER_CURRENT_DOWN ... MOVEMENT_ACTION_RIDE_WATER_CURRENT_RIGHT: - // Handle player on waterfall. - if (PlayerIsUnderWaterfall(&gObjectEvents[gPlayerAvatar.objectEventId]) && IsPlayerSurfingNorth()) - return MOVEMENT_INVALID; - RETURN_STATE(MOVEMENT_ACTION_RIDE_WATER_CURRENT_DOWN, direction); // Acro bike. @@ -1544,37 +1542,20 @@ void FollowerNPC_TryRemoveFollowerOnWhiteOut(void) #undef tDoorY // Task data -#define PREVENT_PLAYER_STEP 0 -#define DO_ALL_FORCED_MOVEMENTS 1 -#define NPC_INTO_PLAYER 2 -#define ENABLE_PLAYER_STEP 3 +#define NPC_INTO_PLAYER 0 +#define ENABLE_PLAYER_STEP 1 void Task_MoveNPCFollowerAfterForcedMovement(u8 taskId) { struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()]; struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId]; - // Prevent player input until all forced mmovements are done and the follower is hidden. - if (gTasks[taskId].tState == PREVENT_PLAYER_STEP) + // The NPC will take an extra step and be on the same tile as the player. + if (gTasks[taskId].tState == NPC_INTO_PLAYER && ObjectEventClearHeldMovementIfFinished(player) != 0 && ObjectEventClearHeldMovementIfFinished(follower) != 0) { - gPlayerAvatar.preventStep = TRUE; - gTasks[taskId].tState = DO_ALL_FORCED_MOVEMENTS; - } - // The player will keep doing forced movments until they land on a non-forced-move metatile or hit collision. - else if (gTasks[taskId].tState == DO_ALL_FORCED_MOVEMENTS && ObjectEventClearHeldMovementIfFinished(player) != 0) - { - // Lock follower facing direction for muddy slope. if (follower->currentMetatileBehavior == MB_MUDDY_SLOPE) follower->facingDirectionLocked = TRUE; - if (TryDoMetatileBehaviorForcedMovement() == 0) - gTasks[taskId].tState = NPC_INTO_PLAYER; - - return; - } - // The NPC will take an extra step and be on the same tile as the player. - else if (gTasks[taskId].tState == NPC_INTO_PLAYER && ObjectEventClearHeldMovementIfFinished(player) != 0 && ObjectEventClearHeldMovementIfFinished(follower) != 0) - { ObjectEventSetHeldMovement(follower, GetWalkFastMovementAction(DetermineFollowerNPCDirection(player, follower))); gTasks[taskId].tState = ENABLE_PLAYER_STEP; return; @@ -1585,14 +1566,13 @@ void Task_MoveNPCFollowerAfterForcedMovement(u8 taskId) follower->facingDirectionLocked = FALSE; HideNPCFollower(); SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_REAPPEAR); + SetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT, FALSE); gPlayerAvatar.preventStep = FALSE; DestroyTask(taskId); } } #undef tState -#undef PREVENT_PLAYER_STEP -#undef DO_ALL_FORCED_MOVEMENTS #undef NPC_INTO_PLAYER #undef ENABLE_PLAYER_STEP @@ -1628,7 +1608,6 @@ void ScriptDestroyFollowerNPC(struct ScriptContext *ctx) return; RemoveObjectEvent(&gObjectEvents[GetFollowerNPCData(FNPC_DATA_OBJ_ID)]); - FlagSet(GetFollowerNPCData(FNPC_DATA_EVENT_FLAG)); ClearFollowerNPCData(); UpdateFollowingPokemon(); } From 7500fc00db63c495054062e99b578b233c95a89a Mon Sep 17 00:00:00 2001 From: RavePossum <145081120+ravepossum@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:24:49 -0400 Subject: [PATCH 08/26] Restore git history check (#7463) --- make_tools.mk | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/make_tools.mk b/make_tools.mk index 75ebc05c96..6b3db7c873 100644 --- a/make_tools.mk +++ b/make_tools.mk @@ -12,10 +12,10 @@ TOOLDIRS := $(TOOL_NAMES:%=$(TOOLS_DIR)/%) CHECKTOOLDIRS := $(CHECK_TOOL_NAMES:%=$(TOOLS_DIR)/%) # Tool making doesnt require a pokeemerald dependency scan. -RULES_NO_SCAN += tools check-tools clean-tools clean-check-tools $(TOOLDIRS) $(CHECKTOOLDIRS) +RULES_NO_SCAN += tools check-tools clean-tools clean-check-tools history $(TOOLDIRS) $(CHECKTOOLDIRS) .PHONY: $(RULES_NO_SCAN) -tools: $(TOOLDIRS) +tools: history $(TOOLDIRS) check-tools: $(CHECKTOOLDIRS) @@ -30,3 +30,6 @@ clean-tools: clean-check-tools: @$(foreach tooldir,$(CHECKTOOLDIRS),$(MAKE) clean -C $(tooldir);) + +history: + @$(SHELL) ./check_history.sh From 27886a6577c2f587e3a79a871a5bf0422660e241 Mon Sep 17 00:00:00 2001 From: grintoul <166724814+grintoul1@users.noreply.github.com> Date: Tue, 5 Aug 2025 13:14:04 +0100 Subject: [PATCH 09/26] Macro-for-AI-Flag-definitions (#7471) --- include/constants/battle_ai.h | 69 +++++++++++++++++------------------ 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/include/constants/battle_ai.h b/include/constants/battle_ai.h index 2f97ab2234..5da20264eb 100644 --- a/include/constants/battle_ai.h +++ b/include/constants/battle_ai.h @@ -3,38 +3,37 @@ // AI Flags. Most run specific functions to update score, new flags are used for internal logic in other scripts // See docs/ai_flags.md for more details. -#define AI_FLAG_CHECK_BAD_MOVE (1 << 0) // AI will avoid using moves that are likely to fail or be ineffective in the current situation. -#define AI_FLAG_TRY_TO_FAINT (1 << 1) // AI will prioritize KOing the player's mon if able. -#define AI_FLAG_CHECK_VIABILITY (1 << 2) // AI damaging moves and move effects to determine the best available move in the current situation. -#define AI_FLAG_FORCE_SETUP_FIRST_TURN (1 << 3) // AI will prioritize using setup moves on the first turn at the expensve of all else. AI_FLAG_CHECK_VIABILITY will instead do this when the AI determines it makes sense. -#define AI_FLAG_RISKY (1 << 4) // AI will generally behave more recklessly, prioritizing damage over accuracy, explosions, etc. -#define AI_FLAG_TRY_TO_2HKO (1 << 5) // AI adds score bonus to any move the AI has that either OHKOs or 2HKOs the player. -#define AI_FLAG_PREFER_BATON_PASS (1 << 6) // AI prefers raising its own stats and setting for / using Baton Pass. -#define AI_FLAG_DOUBLE_BATTLE (1 << 7) // Automatically set for double battles, handles AI behaviour with partner. -#define AI_FLAG_HP_AWARE (1 << 8) // AI will favour certain move effects based on how much remaining HP it and the player's mon have. -#define AI_FLAG_POWERFUL_STATUS (1 << 9) // AI prefers moves that set up field effects or side statuses, even if the user can faint the target. -// New, Trainer Handicap Flags -#define AI_FLAG_NEGATE_UNAWARE (1 << 10) // AI is NOT aware of negating effects like wonder room, mold breaker, etc. -#define AI_FLAG_WILL_SUICIDE (1 << 11) // AI will use explosion / self destruct / final gambit / etc. -// New, Trainer Strategy Flags -#define AI_FLAG_PREFER_STATUS_MOVES (1 << 12) // AI gets a score bonus for status moves. Should be combined with AI_FLAG_CHECK_BAD_MOVE to prevent using only status moves. -#define AI_FLAG_STALL (1 << 13) // AI stalls battle and prefers secondary damage/trapping/etc. TODO not finished. -#define AI_FLAG_SMART_SWITCHING (1 << 14) // AI includes a lot more switching checks. Automatically includes AI_FLAG_SMART_MON_CHOICES. -#define AI_FLAG_ACE_POKEMON (1 << 15) // AI has an Ace Pokemon. The last Pokemon in the party will not be used until it's the last one remaining. -#define AI_FLAG_OMNISCIENT (1 << 16) // AI has full knowledge of player moves, abilities, hold items. -#define AI_FLAG_SMART_MON_CHOICES (1 << 17) // AI will make smarter decisions when choosing which mon to send out mid-battle and after a KO, which are separate decisions. Automatically included by AI_FLAG_SMART_SWITCHING. -#define AI_FLAG_CONSERVATIVE (1 << 18) // AI assumes all moves will low roll damage. -#define AI_FLAG_SEQUENCE_SWITCHING (1 << 19) // AI switches in mons in exactly party order, and never switches mid-battle. -#define AI_FLAG_DOUBLE_ACE_POKEMON (1 << 20) // AI has *two* Ace Pokémon. The last two Pokémons in the party won't be used unless they're the last ones remaining. Goes well in battles where the trainer ID equals to twins, couples, etc. -#define AI_FLAG_WEIGH_ABILITY_PREDICTION (1 << 21) // AI will predict player's ability based on aiRating -#define AI_FLAG_PREFER_HIGHEST_DAMAGE_MOVE (1 << 22) // AI adds score to highest damage move regardless of accuracy or secondary effect -#define AI_FLAG_PREDICT_SWITCH (1 << 23) // AI will predict the player's switches and switchins based on how it would handle the situation. Recommend using AI_FLAG_OMNISCIENT -#define AI_FLAG_PREDICT_INCOMING_MON (1 << 24) // AI will score against the predicting incoming mon if it predicts the player to switch. Requires AI_FLAG_PREDICT_SWITCH -#define AI_FLAG_PP_STALL_PREVENTION (1 << 25) // AI keeps track of the player's switches where the incoming mon is immune to the chosen move -#define AI_FLAG_PREDICT_MOVE (1 << 26) // AI will predict the player's move based on what move it would use in the same situation. Recommend using AI_FLAG_OMNISCIENT +#define AI_FLAG(x) ((u64)1 << x) -// Flags at and after 32 need different formatting, as in -// #define AI_FLAG_PLACEHOLDER ((u64)1 << 32) +#define AI_FLAG_CHECK_BAD_MOVE AI_FLAG(0) // AI will avoid using moves that are likely to fail or be ineffective in the current situation. +#define AI_FLAG_TRY_TO_FAINT AI_FLAG(1) // AI will prioritize KOing the player's mon if able. +#define AI_FLAG_CHECK_VIABILITY AI_FLAG(2) // AI damaging moves and move effects to determine the best available move in the current situation. +#define AI_FLAG_FORCE_SETUP_FIRST_TURN AI_FLAG(3) // AI will prioritize using setup moves on the first turn at the expensve of all else. AI_FLAG_CHECK_VIABILITY will instead do this when the AI determines it makes sense. +#define AI_FLAG_RISKY AI_FLAG(4) // AI will generally behave more recklessly, prioritizing damage over accuracy, explosions, etc. +#define AI_FLAG_TRY_TO_2HKO AI_FLAG(5) // AI adds score bonus to any move the AI has that either OHKOs or 2HKOs the player. +#define AI_FLAG_PREFER_BATON_PASS AI_FLAG(6) // AI prefers raising its own stats and setting for / using Baton Pass. +#define AI_FLAG_DOUBLE_BATTLE AI_FLAG(7) // Automatically set for double battles, handles AI behaviour with partner. +#define AI_FLAG_HP_AWARE AI_FLAG(8) // AI will favour certain move effects based on how much remaining HP it and the player's mon have. +#define AI_FLAG_POWERFUL_STATUS AI_FLAG(9) // AI prefers moves that set up field effects or side statuses, even if the user can faint the target. +// New, Trainer Handicap Flags +#define AI_FLAG_NEGATE_UNAWARE AI_FLAG(10) // AI is NOT aware of negating effects like wonder room, mold breaker, etc. +#define AI_FLAG_WILL_SUICIDE AI_FLAG(11) // AI will use explosion / self destruct / final gambit / etc. +// New, Trainer Strategy Flags +#define AI_FLAG_PREFER_STATUS_MOVES AI_FLAG(12) // AI gets a score bonus for status moves. Should be combined with AI_FLAG_CHECK_BAD_MOVE to prevent using only status moves. +#define AI_FLAG_STALL AI_FLAG(13) // AI stalls battle and prefers secondary damage/trapping/etc. TODO not finished. +#define AI_FLAG_SMART_SWITCHING AI_FLAG(14) // AI includes a lot more switching checks. Automatically includes AI_FLAG_SMART_MON_CHOICES. +#define AI_FLAG_ACE_POKEMON AI_FLAG(15) // AI has an Ace Pokemon. The last Pokemon in the party will not be used until it's the last one remaining. +#define AI_FLAG_OMNISCIENT AI_FLAG(16) // AI has full knowledge of player moves, abilities, hold items. +#define AI_FLAG_SMART_MON_CHOICES AI_FLAG(17) // AI will make smarter decisions when choosing which mon to send out mid-battle and after a KO, which are separate decisions. Automatically included by AI_FLAG_SMART_SWITCHING. +#define AI_FLAG_CONSERVATIVE AI_FLAG(18) // AI assumes all moves will low roll damage. +#define AI_FLAG_SEQUENCE_SWITCHING AI_FLAG(19) // AI switches in mons in exactly party order, and never switches mid-battle. +#define AI_FLAG_DOUBLE_ACE_POKEMON AI_FLAG(20) // AI has *two* Ace Pokémon. The last two Pokémons in the party won't be used unless they're the last ones remaining. Goes well in battles where the trainer ID equals to twins, couples, etc. +#define AI_FLAG_WEIGH_ABILITY_PREDICTION AI_FLAG(21) // AI will predict player's ability based on aiRating +#define AI_FLAG_PREFER_HIGHEST_DAMAGE_MOVE AI_FLAG(22) // AI adds score to highest damage move regardless of accuracy or secondary effect +#define AI_FLAG_PREDICT_SWITCH AI_FLAG(23) // AI will predict the player's switches and switchins based on how it would handle the situation. Recommend using AI_FLAG_OMNISCIENT +#define AI_FLAG_PREDICT_INCOMING_MON AI_FLAG(24) // AI will score against the predicting incoming mon if it predicts the player to switch. Requires AI_FLAG_PREDICT_SWITCH +#define AI_FLAG_PP_STALL_PREVENTION AI_FLAG(25) // AI keeps track of the player's switches where the incoming mon is immune to the chosen move +#define AI_FLAG_PREDICT_MOVE AI_FLAG(26) // AI will predict the player's move based on what move it would use in the same situation. Recommend using AI_FLAG_OMNISCIENT #define AI_FLAG_COUNT 27 @@ -45,10 +44,10 @@ #define AI_FLAG_PREDICTION (AI_FLAG_PREDICT_SWITCH | AI_FLAG_PREDICT_INCOMING_MON | AI_FLAG_PREDICT_MOVE) // 'other' ai logic flags -#define AI_FLAG_DYNAMIC_FUNC ((u64)1 << 60) // Create custom AI functions for specific battles via "setdynamicaifunc" cmd -#define AI_FLAG_ROAMING ((u64)1 << 61) -#define AI_FLAG_SAFARI ((u64)1 << 62) -#define AI_FLAG_FIRST_BATTLE ((u64)1 << 63) +#define AI_FLAG_DYNAMIC_FUNC AI_FLAG(60) // Create custom AI functions for specific battles via "setdynamicaifunc" cmd +#define AI_FLAG_ROAMING AI_FLAG(61) +#define AI_FLAG_SAFARI AI_FLAG(62) +#define AI_FLAG_FIRST_BATTLE AI_FLAG(63) #define AI_SCORE_DEFAULT 100 // Default score for all AI moves. From f8cbea91673ee83089666f69478ddf792f1e259e Mon Sep 17 00:00:00 2001 From: "Danny Wang (ThePeeps191)" <74725787+ThePeeps191@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:50:32 -0600 Subject: [PATCH 10/26] Updating a link to porytiles in INSTALL.md (#7490) --- INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index d2e511dace..d265d588a1 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -179,6 +179,6 @@ If you targeted a specific version that is not the latest version listed on the # Useful additional tools * [porymap](https://github.com/huderlem/porymap) for viewing and editing maps -* [porytiles](https://github.com/gruntlucas/porytiles) for add new metatiles for maps +* [porytiles](https://github.com/grunt-lucas/porytiles) for add new metatiles for maps * [poryscript](https://github.com/huderlem/poryscript) for scripting ([VS Code extension](https://marketplace.visualstudio.com/items?itemName=karathan.poryscript)) * [Tilemap Studio](https://github.com/Rangi42/tilemap-studio) for viewing and editing tilemaps From 1cb053e8e778775c60a32daa85eae392d74b5c7c Mon Sep 17 00:00:00 2001 From: Hedara Date: Tue, 5 Aug 2025 13:39:01 +0200 Subject: [PATCH 11/26] Add test for BoxPokemon integrity --- test/pokemon.c | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/test/pokemon.c b/test/pokemon.c index ac57314f9c..82a95dfd0d 100644 --- a/test/pokemon.c +++ b/test/pokemon.c @@ -4,6 +4,7 @@ #include "pokemon.h" #include "test/overworld_script.h" #include "test/test.h" +#include "constants/characters.h" TEST("Nature independent from Hidden Nature") { @@ -450,3 +451,99 @@ TEST("Pokémon level up learnsets fit within MAX_LEVEL_UP_MOVES and MAX_RELEARNE EXPECT_LT(count, MAX_LEVEL_UP_MOVES); EXPECT_LT(count, MAX_RELEARNER_MOVES - 1); // - 1 because at least one move is already known } + +TEST("BoxPokemon encryption works") +{ + u32 raw[20] = + { + 990384375, + 2948624514, + 3907508686, + 14410461, + 35316705, + 3907508686, + 64742109, + 718729, + 3102307966, + 2160206402, + 49956971, + 2495766612, + 1424318580, + 273408756, + 2371630199, + 2708871082, + 3059937332, + 2529190026, + 2290634828, + 2870614922 + }; + + struct Pokemon mon; + BoxMonToMon((struct BoxPokemon *)&raw, &mon); + + EXPECT_EQ(GetMonData(&mon, MON_DATA_SPECIES), SPECIES_TORCHIC); + EXPECT_EQ(GetMonData(&mon, MON_DATA_MARKINGS), 3); + const u8 *actualNickname = COMPOUND_STRING("Testing mon"); + u8 nickname[12]; + GetMonData(&mon, MON_DATA_NICKNAME, nickname); + u32 charIndex = 0; + while (actualNickname[charIndex] != EOS) + { + EXPECT_EQ(actualNickname[charIndex], nickname[charIndex]); + charIndex++; + } + EXPECT_EQ(GetNature(&mon), NATURE_HARDY); + EXPECT_EQ(GetMonData(&mon, MON_DATA_HIDDEN_NATURE), NATURE_ADAMANT); + EXPECT_EQ(GetMonData(&mon, MON_DATA_HP_LOST), 10); + EXPECT_EQ(GetMonData(&mon, MON_DATA_HELD_ITEM), ITEM_ORAN_BERRY); + EXPECT_EQ(GetMonData(&mon, MON_DATA_MOVE1), MOVE_TACKLE); + EXPECT_EQ(GetMonData(&mon, MON_DATA_MOVE2), MOVE_SCRATCH); + EXPECT_EQ(GetMonData(&mon, MON_DATA_MOVE3), MOVE_POUND); + EXPECT_EQ(GetMonData(&mon, MON_DATA_MOVE4), MOVE_GROWL); + EXPECT_EQ(GetMonData(&mon, MON_DATA_PP1), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_PP2), 2); + EXPECT_EQ(GetMonData(&mon, MON_DATA_PP3), 3); + EXPECT_EQ(GetMonData(&mon, MON_DATA_PP4), 4); + EXPECT_EQ(GetMonData(&mon, MON_DATA_PP_BONUSES), 255); + EXPECT_EQ(GetMonData(&mon, MON_DATA_COOL), 10); + EXPECT_EQ(GetMonData(&mon, MON_DATA_BEAUTY), 20); + EXPECT_EQ(GetMonData(&mon, MON_DATA_CUTE), 30); + EXPECT_EQ(GetMonData(&mon, MON_DATA_SMART), 40); + EXPECT_EQ(GetMonData(&mon, MON_DATA_TOUGH), 50); + EXPECT_EQ(GetMonData(&mon, MON_DATA_SHEEN), 150); + EXPECT_EQ(GetMonData(&mon, MON_DATA_EXP), 12345); + EXPECT_EQ(GetMonData(&mon, MON_DATA_MET_LEVEL), 20); + EXPECT_EQ(GetMonData(&mon, MON_DATA_HP_EV), 11); + EXPECT_EQ(GetMonData(&mon, MON_DATA_ATK_EV), 22); + EXPECT_EQ(GetMonData(&mon, MON_DATA_DEF_EV), 33); + EXPECT_EQ(GetMonData(&mon, MON_DATA_SPEED_EV), 44); + EXPECT_EQ(GetMonData(&mon, MON_DATA_SPATK_EV), 55); + EXPECT_EQ(GetMonData(&mon, MON_DATA_SPDEF_EV), 66); + EXPECT_EQ(GetMonData(&mon, MON_DATA_FRIENDSHIP), 123); + EXPECT_EQ(GetMonData(&mon, MON_DATA_POKERUS), 2); + EXPECT_EQ(GetMonData(&mon, MON_DATA_POKEBALL), BALL_FRIEND); + EXPECT_EQ(GetMonData(&mon, MON_DATA_HP_IV), 31); + EXPECT_EQ(GetMonData(&mon, MON_DATA_ATK_IV), 30); + EXPECT_EQ(GetMonData(&mon, MON_DATA_DEF_IV), 29); + EXPECT_EQ(GetMonData(&mon, MON_DATA_SPEED_IV), 28); + EXPECT_EQ(GetMonData(&mon, MON_DATA_SPATK_IV), 27); + EXPECT_EQ(GetMonData(&mon, MON_DATA_SPDEF_IV), 26); + EXPECT_EQ(GetMonData(&mon, MON_DATA_CUTE_RIBBON), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_BEAUTY_RIBBON), 0); + EXPECT_EQ(GetMonData(&mon, MON_DATA_TOUGH_RIBBON), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_SMART_RIBBON), 0); + EXPECT_EQ(GetMonData(&mon, MON_DATA_CHAMPION_RIBBON), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_VICTORY_RIBBON), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_EFFORT_RIBBON), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_LAND_RIBBON), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_COUNTRY_RIBBON), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_EARTH_RIBBON), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_HYPER_TRAINED_HP), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_HYPER_TRAINED_ATK), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_HYPER_TRAINED_DEF), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_HYPER_TRAINED_SPEED), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_HYPER_TRAINED_SPATK), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_HYPER_TRAINED_SPDEF), 1); + EXPECT_EQ(GetMonData(&mon, MON_DATA_DYNAMAX_LEVEL), 3); + EXPECT_EQ(GetMonData(&mon, MON_DATA_OT_GENDER), 0); +} From e8fefbcc75406118c49c5eab861564ca42c94de0 Mon Sep 17 00:00:00 2001 From: Hedara Date: Tue, 5 Aug 2025 13:49:11 +0200 Subject: [PATCH 12/26] Add bad egg check --- test/pokemon.c | 1 + 1 file changed, 1 insertion(+) diff --git a/test/pokemon.c b/test/pokemon.c index 82a95dfd0d..47ec6af519 100644 --- a/test/pokemon.c +++ b/test/pokemon.c @@ -481,6 +481,7 @@ TEST("BoxPokemon encryption works") struct Pokemon mon; BoxMonToMon((struct BoxPokemon *)&raw, &mon); + EXPECT_EQ(GetMonData(&mon, MON_DATA_SANITY_IS_BAD_EGG), 0); EXPECT_EQ(GetMonData(&mon, MON_DATA_SPECIES), SPECIES_TORCHIC); EXPECT_EQ(GetMonData(&mon, MON_DATA_MARKINGS), 3); const u8 *actualNickname = COMPOUND_STRING("Testing mon"); From 521363ed8b6bf2cbbe0da364f92b95c5697fdd34 Mon Sep 17 00:00:00 2001 From: Eduardo Quezada Date: Thu, 7 Aug 2025 13:43:07 -0400 Subject: [PATCH 13/26] Fixed compile issue `OW_TIMES_OF_DAY` is `GEN_3` (#7499) --- src/overworld.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/overworld.c b/src/overworld.c index d827d46a2c..2cd7d42b4f 100644 --- a/src/overworld.c +++ b/src/overworld.c @@ -916,7 +916,7 @@ static void LoadMapFromWarp(bool32 a1) ClearTempFieldEventData(); ResetDexNavSearch(); // reset hours override on every warp - sHoursOverride = 0; + sHoursOverride = 0; ResetCyclingRoadChallengeData(); RestartWildEncounterImmunitySteps(); #if FREE_MATCH_CALL == FALSE @@ -1559,7 +1559,7 @@ const struct BlendSettings gTimeOfDayBlend[] = }; #define DEFAULT_WEIGHT 256 -#define TIME_BLEND_WEIGHT(begin, end) (DEFAULT_WEIGHT - (DEFAULT_WEIGHT * ((hours - begin) * MINUTES_PER_HOUR + minutes) / ((end - begin) * MINUTES_PER_HOUR))) +#define TIME_BLEND_WEIGHT(begin, end) (DEFAULT_WEIGHT - (DEFAULT_WEIGHT * SAFE_DIV(((hours - begin) * MINUTES_PER_HOUR + minutes), ((end - begin) * MINUTES_PER_HOUR)))) #define MORNING_HOUR_MIDDLE (MORNING_HOUR_BEGIN + ((MORNING_HOUR_END - MORNING_HOUR_BEGIN) / 2)) From bef3e990ef665e2845a5b958400a74a64a05ebb2 Mon Sep 17 00:00:00 2001 From: Eduardo Quezada Date: Thu, 7 Aug 2025 13:47:16 -0400 Subject: [PATCH 14/26] Fixes shadows and mon animations for transformed battlers (#7500) --- src/battle_gfx_sfx_util.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/battle_gfx_sfx_util.c b/src/battle_gfx_sfx_util.c index 9b272a51b4..6ca847a272 100644 --- a/src/battle_gfx_sfx_util.c +++ b/src/battle_gfx_sfx_util.c @@ -1272,8 +1272,8 @@ void SpriteCB_EnemyShadow(struct Sprite *shadowSprite) } else if (transformSpecies != SPECIES_NONE) { - xOffset = gSpeciesInfo[transformSpecies].enemyShadowXOffset; - yOffset = gSpeciesInfo[transformSpecies].enemyShadowYOffset; + xOffset = gSpeciesInfo[transformSpecies].enemyShadowXOffset + (shadowSprite->tSpriteSide == SPRITE_SIDE_LEFT ? -16 : 16); + yOffset = gSpeciesInfo[transformSpecies].enemyShadowYOffset + 16; size = gSpeciesInfo[transformSpecies].enemyShadowSize; invisible = (B_ENEMY_MON_SHADOW_STYLE >= GEN_4 && P_GBA_STYLE_SPECIES_GFX == FALSE) From 729abca05887e35484a8575324abdb48c10b4f50 Mon Sep 17 00:00:00 2001 From: RavePossum <145081120+ravepossum@users.noreply.github.com> Date: Thu, 7 Aug 2025 13:53:01 -0400 Subject: [PATCH 15/26] Fix using town map as registered item (#7482) --- src/item_use.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/item_use.c b/src/item_use.c index 2c1f892197..3d2c828786 100644 --- a/src/item_use.c +++ b/src/item_use.c @@ -1609,8 +1609,7 @@ void ItemUseOutOfBattle_TownMap(u8 taskId) } else { - // TODO: handle key items with callbacks to menus allow to be used by registering them. - DisplayDadsAdviceCannotUseItemMessage(taskId, gTasks[taskId].tUsingRegisteredKeyItem); + gTasks[taskId].func = ItemUseOnFieldCB_TownMap; } } From bf68865e0d88aca9b781cbd8078b59a53c949120 Mon Sep 17 00:00:00 2001 From: RavePossum <145081120+ravepossum@users.noreply.github.com> Date: Thu, 7 Aug 2025 15:21:33 -0400 Subject: [PATCH 16/26] Implement using party menu items from field (#7481) --- include/party_menu.h | 4 +- src/item_use.c | 101 ++++++++++++++++++------------------------- src/party_menu.c | 6 +++ 3 files changed, 50 insertions(+), 61 deletions(-) diff --git a/include/party_menu.h b/include/party_menu.h index 7c4af8c3a4..80deab2a85 100644 --- a/include/party_menu.h +++ b/include/party_menu.h @@ -16,10 +16,12 @@ struct PartyMenu s8 slotId2; u8 action; u16 bagItem; - s16 data1; // used variously as a move, counter, moveSlotId, or cursorPos + s16 data1; // used variously as a move, counter, moveSlotId, cursorPos, or indicator that the menu is opened from the field s16 learnMoveState; // data2, used only as a learn move state }; +#define DATA1_PARTY_MENU_FROM_FIELD -1 + extern struct PartyMenu gPartyMenu; extern bool8 gPartyMenuUseExitCallback; extern u8 gSelectedMonPartyId; diff --git a/src/item_use.c b/src/item_use.c index 3d2c828786..0a8a5be06e 100644 --- a/src/item_use.c +++ b/src/item_use.c @@ -50,6 +50,7 @@ static void SetUpItemUseCallback(u8); static void FieldCB_UseItemOnField(void); static void Task_CallItemUseOnFieldCallback(u8); +static void Task_PartyMenuItemUseFromField(u8); static void Task_UseItemfinder(u8); static void Task_CloseItemfinderMessage(u8); static void Task_HiddenItemNearby(u8); @@ -128,15 +129,25 @@ static void SetUpItemUseCallback(u8 taskId) type = gTasks[taskId].tEnigmaBerryType - 1; else type = GetItemType(gSpecialVar_ItemId) - 1; - if (CurrentBattlePyramidLocation() == PYRAMID_LOCATION_NONE) + + if (gTasks[taskId].tUsingRegisteredKeyItem && type == (ITEM_USE_PARTY_MENU - 1)) { - gBagMenu->newScreenCallback = sItemUseCallbacks[type]; - Task_FadeAndCloseBagMenu(taskId); + FadeScreen(FADE_TO_BLACK, 0); + gPartyMenu.data1 = DATA1_PARTY_MENU_FROM_FIELD; + gTasks[taskId].func = Task_PartyMenuItemUseFromField; } else { - gPyramidBagMenu->newScreenCallback = sItemUseCallbacks[type]; - CloseBattlePyramidBag(taskId); + if (CurrentBattlePyramidLocation() == PYRAMID_LOCATION_NONE) + { + gBagMenu->newScreenCallback = sItemUseCallbacks[type]; + Task_FadeAndCloseBagMenu(taskId); + } + else + { + gPyramidBagMenu->newScreenCallback = sItemUseCallbacks[type]; + CloseBattlePyramidBag(taskId); + } } } @@ -165,6 +176,16 @@ static void Task_CallItemUseOnFieldCallback(u8 taskId) sItemUseOnFieldCB(taskId); } +static void Task_PartyMenuItemUseFromField(u8 taskId) +{ + if (!gPaletteFade.active) + { + CleanupOverworldWindowsAndTilemaps(); + SetMainCallback2(CB2_ShowPartyMenuForItemUse); + DestroyTask(taskId); + } +} + static void DisplayCannotUseItemMessage(u8 taskId, bool8 isUsingRegisteredKeyItemOnField, const u8 *str) { StringExpandPlaceholders(gStringVar4, str); @@ -1380,77 +1401,37 @@ void ItemUseOutOfBattle_EnigmaBerry(u8 taskId) void ItemUseOutOfBattle_FormChange(u8 taskId) { - if (!gTasks[taskId].tUsingRegisteredKeyItem) - { - gItemUseCB = ItemUseCB_FormChange; - gTasks[taskId].data[0] = FALSE; - SetUpItemUseOnFieldCallback(taskId); - } - else - { - // TODO: handle key items with callbacks to menus allow to be used by registering them. - DisplayDadsAdviceCannotUseItemMessage(taskId, gTasks[taskId].tUsingRegisteredKeyItem); - } + gItemUseCB = ItemUseCB_FormChange; + gTasks[taskId].data[0] = FALSE; + SetUpItemUseCallback(taskId); } void ItemUseOutOfBattle_FormChange_ConsumedOnUse(u8 taskId) { - if (!gTasks[taskId].tUsingRegisteredKeyItem) - { - gItemUseCB = ItemUseCB_FormChange_ConsumedOnUse; - gTasks[taskId].data[0] = TRUE; - SetUpItemUseOnFieldCallback(taskId); - } - else - { - // TODO: handle key items with callbacks to menus allow to be used by registering them. - DisplayDadsAdviceCannotUseItemMessage(taskId, gTasks[taskId].tUsingRegisteredKeyItem); - } + gItemUseCB = ItemUseCB_FormChange_ConsumedOnUse; + gTasks[taskId].data[0] = TRUE; + SetUpItemUseCallback(taskId); } void ItemUseOutOfBattle_RotomCatalog(u8 taskId) { - if (!gTasks[taskId].tUsingRegisteredKeyItem) - { - gItemUseCB = ItemUseCB_RotomCatalog; - gTasks[taskId].data[0] = TRUE; - SetUpItemUseOnFieldCallback(taskId); - } - else - { - // TODO: handle key items with callbacks to menus allow to be used by registering them. - DisplayDadsAdviceCannotUseItemMessage(taskId, gTasks[taskId].tUsingRegisteredKeyItem); - } + gItemUseCB = ItemUseCB_RotomCatalog; + gTasks[taskId].data[0] = TRUE; + SetUpItemUseCallback(taskId); } void ItemUseOutOfBattle_ZygardeCube(u8 taskId) { - if (!gTasks[taskId].tUsingRegisteredKeyItem) - { - gItemUseCB = ItemUseCB_ZygardeCube; - gTasks[taskId].data[0] = TRUE; - SetUpItemUseOnFieldCallback(taskId); - } - else - { - // TODO: handle key items with callbacks to menus allow to be used by registering them. - DisplayDadsAdviceCannotUseItemMessage(taskId, gTasks[taskId].tUsingRegisteredKeyItem); - } + gItemUseCB = ItemUseCB_ZygardeCube; + gTasks[taskId].data[0] = TRUE; + SetUpItemUseCallback(taskId); } void ItemUseOutOfBattle_Fusion(u8 taskId) { - if (!gTasks[taskId].tUsingRegisteredKeyItem) - { - gItemUseCB = ItemUseCB_Fusion; - gTasks[taskId].data[0] = FALSE; - SetUpItemUseCallback(taskId); - } - else - { - // TODO: handle key items with callbacks to menus allow to be used by registering them. - DisplayDadsAdviceCannotUseItemMessage(taskId, gTasks[taskId].tUsingRegisteredKeyItem); - } + gItemUseCB = ItemUseCB_Fusion; + gTasks[taskId].data[0] = FALSE; + SetUpItemUseCallback(taskId); } void Task_UseHoneyOnField(u8 taskId) diff --git a/src/party_menu.c b/src/party_menu.c index 146a06cb69..419909407a 100644 --- a/src/party_menu.c +++ b/src/party_menu.c @@ -4559,6 +4559,12 @@ void CB2_ShowPartyMenuForItemUse(void) u8 msgId; TaskFunc task; + if (gPartyMenu.data1 == DATA1_PARTY_MENU_FROM_FIELD) + { + callback = CB2_ReturnToField; + gPartyMenu.data1 = 0; + } + if (gMain.inBattle) { menuType = PARTY_MENU_TYPE_IN_BATTLE; From 27539239dbcf152ae473a8654a3fe3ec4f66c539 Mon Sep 17 00:00:00 2001 From: Hedara Date: Sat, 2 Aug 2025 16:25:11 +0200 Subject: [PATCH 17/26] Clear out parties between battle tests --- test/test_runner_battle.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/test_runner_battle.c b/test/test_runner_battle.c index 20271a588d..bfbf4c899d 100644 --- a/test/test_runner_battle.c +++ b/test/test_runner_battle.c @@ -1341,6 +1341,18 @@ void TestRunner_Battle_AfterLastTurn(void) static void TearDownBattle(void) { + // Zero out the parties, data in them could potentially carry over + for (u32 i = 0; i < 6; i++) + { + u32 *playerPtr = (u32 *)(&gPlayerParty[i]); + u32 *opponentPtr = (u32 *)(&gEnemyParty[i]); + for (u32 j = 0; j < sizeof(struct Pokemon)/4; j++) + { + playerPtr[j] = 0; + opponentPtr[j] = 0; + } + } + FreeMonSpritesGfx(); FreeBattleSpritesData(); FreeBattleResources(); From b553278a3d79ccab0e1b46bba695e9be91c1e1ac Mon Sep 17 00:00:00 2001 From: Hedara Date: Sat, 2 Aug 2025 16:33:42 +0200 Subject: [PATCH 18/26] Replaced manual zeroing with existing function calls --- test/test_runner_battle.c | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/test/test_runner_battle.c b/test/test_runner_battle.c index bfbf4c899d..0e78d44ee4 100644 --- a/test/test_runner_battle.c +++ b/test/test_runner_battle.c @@ -1344,13 +1344,8 @@ static void TearDownBattle(void) // Zero out the parties, data in them could potentially carry over for (u32 i = 0; i < 6; i++) { - u32 *playerPtr = (u32 *)(&gPlayerParty[i]); - u32 *opponentPtr = (u32 *)(&gEnemyParty[i]); - for (u32 j = 0; j < sizeof(struct Pokemon)/4; j++) - { - playerPtr[j] = 0; - opponentPtr[j] = 0; - } + ZeroPlayerPartyMons(); + ZeroEnemyPartyMons(); } FreeMonSpritesGfx(); From 8d8cf1b5d489efeb922a9672d25a8d633383744e Mon Sep 17 00:00:00 2001 From: Hedara Date: Sat, 2 Aug 2025 16:34:36 +0200 Subject: [PATCH 19/26] Removed now unnecessary loop --- test/test_runner_battle.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/test_runner_battle.c b/test/test_runner_battle.c index 0e78d44ee4..ea26432137 100644 --- a/test/test_runner_battle.c +++ b/test/test_runner_battle.c @@ -1342,11 +1342,8 @@ void TestRunner_Battle_AfterLastTurn(void) static void TearDownBattle(void) { // Zero out the parties, data in them could potentially carry over - for (u32 i = 0; i < 6; i++) - { - ZeroPlayerPartyMons(); - ZeroEnemyPartyMons(); - } + ZeroPlayerPartyMons(); + ZeroEnemyPartyMons(); FreeMonSpritesGfx(); FreeBattleSpritesData(); From 0b299b3ffd91ce2b7934c95406405a1368dd9c1c Mon Sep 17 00:00:00 2001 From: Eduardo Quezada Date: Fri, 8 Aug 2025 03:44:12 -0400 Subject: [PATCH 20/26] Fixed most failed tests with `GEN_LATEST GEN_8` (#7498) --- include/constants/generational_changes.h | 8 +++++ include/generational_changes.h | 8 +++++ src/battle_ai_switch_items.c | 2 +- src/battle_ai_util.c | 4 +-- src/battle_script_commands.c | 16 ++++----- src/battle_util.c | 12 +++---- test/battle/ability/dauntless_shield.c | 31 +++++++++++++---- test/battle/ability/intrepid_sword.c | 30 ++++++++++++---- test/battle/ability/keen_eye.c | 7 +++- test/battle/ability/pickup.c | 4 +-- test/battle/ability/protean.c | 34 +++++++++++++++---- test/battle/ability/refrigerate.c | 1 + test/battle/ability/snow_warning.c | 26 ++++++++------ test/battle/ai/ai_switching.c | 1 + test/battle/move_effect/ally_switch.c | 21 ++++++++++-- test/battle/move_effect/chloroblast.c | 7 +++- .../battle/move_effect_secondary/steal_item.c | 4 +-- 17 files changed, 161 insertions(+), 55 deletions(-) diff --git a/include/constants/generational_changes.h b/include/constants/generational_changes.h index 060d4167ef..a1e0a43074 100644 --- a/include/constants/generational_changes.h +++ b/include/constants/generational_changes.h @@ -17,6 +17,14 @@ enum GenConfigTag GEN_CONFIG_BATTLE_BOND, GEN_CONFIG_ATE_MULTIPLIER, GEN_CONFIG_FELL_STINGER_STAT_RAISE, + GEN_PICKUP_WILD, + GEN_PROTEAN_LIBERO, + GEN_INTREPID_SWORD, + GEN_DAUNTLESS_SHIELD, + GEN_ILLUMINATE_EFFECT, + GEN_STEAL_WILD_ITEMS, + GEN_SNOW_WARNING, + GEN_ALLY_SWITCH_FAIL_CHANCE, GEN_CONFIG_COUNT }; diff --git a/include/generational_changes.h b/include/generational_changes.h index 2a97728d0d..87f8586cff 100644 --- a/include/generational_changes.h +++ b/include/generational_changes.h @@ -20,6 +20,14 @@ static const u8 sGenerationalChanges[GEN_CONFIG_COUNT] = [GEN_CONFIG_BATTLE_BOND] = B_BATTLE_BOND, [GEN_CONFIG_FELL_STINGER_STAT_RAISE] = B_FELL_STINGER_STAT_RAISE, [GEN_CONFIG_ATE_MULTIPLIER] = B_ATE_MULTIPLIER, + [GEN_PICKUP_WILD] = B_PICKUP_WILD, + [GEN_PROTEAN_LIBERO] = B_PROTEAN_LIBERO, + [GEN_INTREPID_SWORD] = B_INTREPID_SWORD, + [GEN_DAUNTLESS_SHIELD] = B_DAUNTLESS_SHIELD, + [GEN_ILLUMINATE_EFFECT] = B_ILLUMINATE_EFFECT, + [GEN_STEAL_WILD_ITEMS] = B_STEAL_WILD_ITEMS, + [GEN_SNOW_WARNING] = B_SNOW_WARNING, + [GEN_ALLY_SWITCH_FAIL_CHANCE] = B_ALLY_SWITCH_FAIL_CHANCE, }; #if TESTING diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index c044ff2804..aa7d819a1e 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -691,7 +691,7 @@ static bool32 ShouldSwitchIfBadlyStatused(u32 battler) && gAiLogicData->abilities[opposingBattler] != ABILITY_UNAWARE && gAiLogicData->abilities[opposingBattler] != ABILITY_KEEN_EYE && gAiLogicData->abilities[opposingBattler] != ABILITY_MINDS_EYE - && (B_ILLUMINATE_EFFECT >= GEN_9 && gAiLogicData->abilities[opposingBattler] != ABILITY_ILLUMINATE) + && (GetGenConfig(GEN_ILLUMINATE_EFFECT) >= GEN_9 && gAiLogicData->abilities[opposingBattler] != ABILITY_ILLUMINATE) && !(gBattleMons[battler].status2 & STATUS2_FORESIGHT) && !(gStatuses3[battler] & STATUS3_MIRACLE_EYED)) switchMon = FALSE; diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 7b5e8e0d37..667c6199a4 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -1945,7 +1945,7 @@ bool32 ShouldLowerStat(u32 battlerAtk, u32 battlerDef, u32 abilityDef, u32 stat) if (stat == STAT_DEF) return FALSE; case ABILITY_ILLUMINATE: - if (B_ILLUMINATE_EFFECT < GEN_9) + if (GetGenConfig(GEN_ILLUMINATE_EFFECT) < GEN_9) break; case ABILITY_KEEN_EYE: case ABILITY_MINDS_EYE: @@ -2128,7 +2128,7 @@ bool32 ShouldLowerAccuracy(u32 battlerAtk, u32 battlerDef, u32 defAbility) && defAbility != ABILITY_FULL_METAL_BODY && defAbility != ABILITY_KEEN_EYE && defAbility != ABILITY_MINDS_EYE - && (B_ILLUMINATE_EFFECT >= GEN_9 && defAbility != ABILITY_ILLUMINATE) + && (GetGenConfig(GEN_ILLUMINATE_EFFECT) >= GEN_9 && defAbility != ABILITY_ILLUMINATE) && gAiLogicData->holdEffects[battlerDef] != HOLD_EFFECT_CLEAR_AMULET) return TRUE; return FALSE; diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index d749d450d7..0f3fbdba61 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -1584,7 +1584,7 @@ u32 GetTotalAccuracy(u32 battlerAtk, u32 battlerDef, u32 move, u32 atkAbility, u accStage = gBattleMons[battlerAtk].statStages[STAT_ACC]; evasionStage = gBattleMons[battlerDef].statStages[STAT_EVASION]; if (atkAbility == ABILITY_UNAWARE || atkAbility == ABILITY_KEEN_EYE || atkAbility == ABILITY_MINDS_EYE - || (B_ILLUMINATE_EFFECT >= GEN_9 && atkAbility == ABILITY_ILLUMINATE)) + || (GetGenConfig(GEN_ILLUMINATE_EFFECT) >= GEN_9 && atkAbility == ABILITY_ILLUMINATE)) evasionStage = DEFAULT_STAT_STAGE; if (MoveIgnoresDefenseEvasionStages(move)) evasionStage = DEFAULT_STAT_STAGE; @@ -3137,7 +3137,7 @@ void StealTargetItem(u8 battlerStealer, u8 battlerItem) gLastUsedItem = gBattleMons[battlerItem].item; gBattleMons[battlerItem].item = ITEM_NONE; - if (B_STEAL_WILD_ITEMS >= GEN_9 + if (GetGenConfig(GEN_STEAL_WILD_ITEMS) >= GEN_9 && !(gBattleTypeFlags & (BATTLE_TYPE_TRAINER | BATTLE_TYPE_PALACE)) && GetMoveEffect(gCurrentMove) == EFFECT_STEAL_ITEM && battlerStealer == gBattlerAttacker) // ensure that Pickpocket isn't activating this @@ -5971,7 +5971,7 @@ static void Cmd_playstatchangeanimation(void) && ability != ABILITY_FULL_METAL_BODY && ability != ABILITY_WHITE_SMOKE && !((ability == ABILITY_KEEN_EYE || ability == ABILITY_MINDS_EYE) && currStat == STAT_ACC) - && !(B_ILLUMINATE_EFFECT >= GEN_9 && ability == ABILITY_ILLUMINATE && currStat == STAT_ACC) + && !(GetGenConfig(GEN_ILLUMINATE_EFFECT) >= GEN_9 && ability == ABILITY_ILLUMINATE && currStat == STAT_ACC) && !(ability == ABILITY_HYPER_CUTTER && currStat == STAT_ATK) && !(ability == ABILITY_BIG_PECKS && currStat == STAT_DEF)) { @@ -6303,7 +6303,7 @@ static bool32 HandleMoveEndMoveBlock(u32 moveEffect) { StealTargetItem(gBattlerAttacker, gBattlerTarget); // Attacker steals target item - if (!(B_STEAL_WILD_ITEMS >= GEN_9 + if (!(GetGenConfig(GEN_STEAL_WILD_ITEMS) >= GEN_9 && !(gBattleTypeFlags & (BATTLE_TYPE_TRAINER | BATTLE_TYPE_PALACE)))) { gBattleMons[gBattlerAttacker].item = ITEM_NONE; // Item assigned later on with thief (see MOVEEND_CHANGED_ITEMS) @@ -11715,8 +11715,8 @@ static void TryResetProtectUseCounter(u32 battler) enum BattleMoveEffects lastEffect = GetMoveEffect(lastMove); if (lastMove == MOVE_UNAVAILABLE || (!gBattleMoveEffects[lastEffect].usesProtectCounter - && ((B_ALLY_SWITCH_FAIL_CHANCE >= GEN_9 && lastEffect != EFFECT_ALLY_SWITCH) - || B_ALLY_SWITCH_FAIL_CHANCE < GEN_9))) + && ((GetGenConfig(GEN_ALLY_SWITCH_FAIL_CHANCE) >= GEN_9 && lastEffect != EFFECT_ALLY_SWITCH) + || GetGenConfig(GEN_ALLY_SWITCH_FAIL_CHANCE) < GEN_9))) gDisableStructs[battler].protectUses = 0; } @@ -12418,7 +12418,7 @@ static u32 ChangeStatBuffs(s8 statValue, u32 statId, u32 flags, const u8 *BS_ptr } else if (!certain && (((battlerAbility == ABILITY_KEEN_EYE || battlerAbility == ABILITY_MINDS_EYE) && statId == STAT_ACC) - || (B_ILLUMINATE_EFFECT >= GEN_9 && battlerAbility == ABILITY_ILLUMINATE && statId == STAT_ACC) + || (GetGenConfig(GEN_ILLUMINATE_EFFECT) >= GEN_9 && battlerAbility == ABILITY_ILLUMINATE && statId == STAT_ACC) || (battlerAbility == ABILITY_HYPER_CUTTER && statId == STAT_ATK) || (battlerAbility == ABILITY_BIG_PECKS && statId == STAT_DEF))) { @@ -17759,7 +17759,7 @@ void BS_TryAllySwitch(void) { gBattlescriptCurrInstr = cmd->failInstr; } - else if (B_ALLY_SWITCH_FAIL_CHANCE >= GEN_9) + else if (GetGenConfig(GEN_ALLY_SWITCH_FAIL_CHANCE) >= GEN_9) { TryResetProtectUseCounter(gBattlerAttacker); if (sProtectSuccessRates[gDisableStructs[gBattlerAttacker].protectUses] < Random()) diff --git a/src/battle_util.c b/src/battle_util.c index fb28a6129c..31c5229d0f 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -2311,7 +2311,7 @@ static void CancellerProtean(u32 *effect) u32 moveType = GetBattleMoveType(gCurrentMove); if (ProteanTryChangeType(gBattlerAttacker, GetBattlerAbility(gBattlerAttacker), gCurrentMove, moveType)) { - if (B_PROTEAN_LIBERO == GEN_9) + if (GetGenConfig(GEN_PROTEAN_LIBERO) >= GEN_9) gDisableStructs[gBattlerAttacker].usedProteanLibero = TRUE; PREPARE_TYPE_BUFFER(gBattleTextBuff1, moveType); gBattlerAbility = gBattlerAttacker; @@ -3785,12 +3785,12 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 } break; case ABILITY_SNOW_WARNING: - if (B_SNOW_WARNING >= GEN_9 && TryChangeBattleWeather(battler, BATTLE_WEATHER_SNOW, TRUE)) + if (GetGenConfig(GEN_SNOW_WARNING) >= GEN_9 && TryChangeBattleWeather(battler, BATTLE_WEATHER_SNOW, TRUE)) { BattleScriptPushCursorAndCallback(BattleScript_SnowWarningActivatesSnow); effect++; } - else if (B_SNOW_WARNING < GEN_9 && TryChangeBattleWeather(battler, BATTLE_WEATHER_HAIL, TRUE)) + else if (GetGenConfig(GEN_SNOW_WARNING) < GEN_9 && TryChangeBattleWeather(battler, BATTLE_WEATHER_HAIL, TRUE)) { BattleScriptPushCursorAndCallback(BattleScript_SnowWarningActivatesHail); effect++; @@ -3890,7 +3890,7 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 { gBattleScripting.savedBattler = gBattlerAttacker; gBattlerAttacker = battler; - if (B_INTREPID_SWORD == GEN_9) + if (GetGenConfig(GEN_INTREPID_SWORD) >= GEN_9) gBattleStruct->partyState[GetBattlerSide(battler)][gBattlerPartyIndexes[battler]].intrepidSwordBoost = TRUE; gSpecialStatuses[battler].switchInAbilityDone = TRUE; SET_STATCHANGER(STAT_ATK, 1, FALSE); @@ -3904,7 +3904,7 @@ u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 { gBattleScripting.savedBattler = gBattlerAttacker; gBattlerAttacker = battler; - if (B_DAUNTLESS_SHIELD == GEN_9) + if (GetGenConfig(GEN_DAUNTLESS_SHIELD) >= GEN_9) gBattleStruct->partyState[GetBattlerSide(battler)][gBattlerPartyIndexes[battler]].dauntlessShieldBoost = TRUE; gSpecialStatuses[battler].switchInAbilityDone = TRUE; SET_STATCHANGER(STAT_DEF, 1, FALSE); @@ -10889,7 +10889,7 @@ u16 GetUsedHeldItem(u32 battler) bool32 CantPickupItem(u32 battler) { // Used by RandomUniformExcept() for RNG_PICKUP - if (battler == gBattlerAttacker && (B_PICKUP_WILD < GEN_9 || gBattleTypeFlags & (BATTLE_TYPE_TRAINER | BATTLE_TYPE_LINK))) + if (battler == gBattlerAttacker && (GetGenConfig(GEN_PICKUP_WILD) < GEN_9 || gBattleTypeFlags & (BATTLE_TYPE_TRAINER | BATTLE_TYPE_LINK))) return TRUE; return !(IsBattlerAlive(battler) && GetUsedHeldItem(battler) && gBattleStruct->battlerState[battler].canPickupItem); } diff --git a/test/battle/ability/dauntless_shield.c b/test/battle/ability/dauntless_shield.c index ada4ace786..6fed2f0b5c 100644 --- a/test/battle/ability/dauntless_shield.c +++ b/test/battle/ability/dauntless_shield.c @@ -1,11 +1,6 @@ #include "global.h" #include "test/battle.h" -ASSUMPTIONS -{ - ASSUME(B_DAUNTLESS_SHIELD == GEN_9); -} - SINGLE_BATTLE_TEST("Dauntless Shield raises Defense by one stage") { GIVEN { @@ -22,9 +17,32 @@ SINGLE_BATTLE_TEST("Dauntless Shield raises Defense by one stage") } } -SINGLE_BATTLE_TEST("Dauntless Shield raises Defense by one stage only once per battle") +SINGLE_BATTLE_TEST("Dauntless Shield raises Defense by one stage every time it switches in (Gen8)") { GIVEN { + WITH_CONFIG(GEN_DAUNTLESS_SHIELD, GEN_8); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_ZAMAZENTA) { Ability(ABILITY_DAUNTLESS_SHIELD); } + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { SWITCH(opponent, 1); } + TURN { SWITCH(opponent, 0); } + } SCENE { + ABILITY_POPUP(opponent, ABILITY_DAUNTLESS_SHIELD); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("The opposing Zamazenta's Dauntless Shield raised its Defense!"); + ABILITY_POPUP(opponent, ABILITY_DAUNTLESS_SHIELD); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("The opposing Zamazenta's Dauntless Shield raised its Defense!"); + } THEN { + EXPECT_EQ(opponent->statStages[STAT_DEF], DEFAULT_STAT_STAGE + 1); + } +} + +SINGLE_BATTLE_TEST("Dauntless Shield raises Defense by one stage only once per battle (Gen 9+)") +{ + GIVEN { + WITH_CONFIG(GEN_DAUNTLESS_SHIELD, GEN_9); PLAYER(SPECIES_WOBBUFFET); OPPONENT(SPECIES_ZAMAZENTA) { Ability(ABILITY_DAUNTLESS_SHIELD); } OPPONENT(SPECIES_WYNAUT); @@ -63,4 +81,3 @@ SINGLE_BATTLE_TEST("Dauntless Shield activates when it's no longer effected by N MESSAGE("The opposing Zamazenta's Dauntless Shield raised its Defense!"); } } - diff --git a/test/battle/ability/intrepid_sword.c b/test/battle/ability/intrepid_sword.c index 58fd9883eb..a260e78d0d 100644 --- a/test/battle/ability/intrepid_sword.c +++ b/test/battle/ability/intrepid_sword.c @@ -1,11 +1,6 @@ #include "global.h" #include "test/battle.h" -ASSUMPTIONS -{ - ASSUME(B_INTREPID_SWORD == GEN_9); -} - SINGLE_BATTLE_TEST("Intrepid Sword raises Attack by one stage") { GIVEN { @@ -22,9 +17,32 @@ SINGLE_BATTLE_TEST("Intrepid Sword raises Attack by one stage") } } -SINGLE_BATTLE_TEST("Intrepid Sword raises Attack by one stage only once per battle") +SINGLE_BATTLE_TEST("Intrepid Sword raises Attack by one stage every time it switches in (Gen8)") { GIVEN { + WITH_CONFIG(GEN_INTREPID_SWORD, GEN_8); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_ZACIAN) { Ability(ABILITY_INTREPID_SWORD); } + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { SWITCH(opponent, 1); } + TURN { SWITCH(opponent, 0); } + } SCENE { + ABILITY_POPUP(opponent, ABILITY_INTREPID_SWORD); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("The opposing Zacian's Intrepid Sword raised its Attack!"); + ABILITY_POPUP(opponent, ABILITY_INTREPID_SWORD); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("The opposing Zacian's Intrepid Sword raised its Attack!"); + } THEN { + EXPECT_EQ(opponent->statStages[STAT_ATK], DEFAULT_STAT_STAGE + 1); + } +} + +SINGLE_BATTLE_TEST("Intrepid Sword raises Attack by one stage only once per battle (Gen9+)") +{ + GIVEN { + WITH_CONFIG(GEN_INTREPID_SWORD, GEN_9); PLAYER(SPECIES_WOBBUFFET); OPPONENT(SPECIES_ZACIAN) { Ability(ABILITY_INTREPID_SWORD); } OPPONENT(SPECIES_WYNAUT); diff --git a/test/battle/ability/keen_eye.c b/test/battle/ability/keen_eye.c index 6874e5a6bf..ee10c446fb 100644 --- a/test/battle/ability/keen_eye.c +++ b/test/battle/ability/keen_eye.c @@ -5,7 +5,6 @@ ASSUMPTIONS { ASSUME(GetMoveAccuracy(MOVE_SCRATCH) == 100); ASSUME(GetMoveEffect(MOVE_SAND_ATTACK) == EFFECT_ACCURACY_DOWN); - ASSUME(B_ILLUMINATE_EFFECT >= GEN_9); } SINGLE_BATTLE_TEST("Keen Eye, Gen9+ Illuminate & Minds Eye prevent accuracy stage reduction from moves") @@ -19,6 +18,7 @@ SINGLE_BATTLE_TEST("Keen Eye, Gen9+ Illuminate & Minds Eye prevent accuracy stag PASSES_RANDOMLY(100, 100, RNG_ACCURACY); GIVEN { + WITH_CONFIG(GEN_ILLUMINATE_EFFECT, GEN_9); PLAYER(SPECIES_WOBBUFFET); OPPONENT(species) { Ability(ability); } } WHEN { @@ -47,6 +47,7 @@ SINGLE_BATTLE_TEST("Keen Eye, Gen9+ Illuminate & Minds Eye ignore target's evasi PASSES_RANDOMLY(100, 100, RNG_ACCURACY); GIVEN { + WITH_CONFIG(GEN_ILLUMINATE_EFFECT, GEN_9); ASSUME(GetMoveEffect(MOVE_DOUBLE_TEAM) == EFFECT_EVASION_UP); PLAYER(SPECIES_WOBBUFFET); OPPONENT(species) { Ability(ability); } @@ -80,6 +81,7 @@ SINGLE_BATTLE_TEST("Keen Eye, Gen9+ Illuminate & Minds Eye are ignored by Mold B PASSES_RANDOMLY(GetMoveAccuracy(MOVE_SCRATCH) * 3 / 4, 100, RNG_ACCURACY); GIVEN { + WITH_CONFIG(GEN_ILLUMINATE_EFFECT, GEN_9); PLAYER(speciesPlayer) { Ability(abilityPlayer); } OPPONENT(speciesOpponent) { Ability(abilityOpponent); } } WHEN { @@ -102,6 +104,7 @@ SINGLE_BATTLE_TEST("Keen Eye, Gen9+ Illuminate & Minds Eye don't prevent Topsy-T PARAMETRIZE { species = SPECIES_URSALUNA_BLOODMOON; ability = ABILITY_MINDS_EYE; } GIVEN { + WITH_CONFIG(GEN_ILLUMINATE_EFFECT, GEN_9); ASSUME(GetMoveEffect(MOVE_HONE_CLAWS) == EFFECT_ATTACK_ACCURACY_UP); ASSUME(GetMoveEffect(MOVE_TOPSY_TURVY) == EFFECT_TOPSY_TURVY); PLAYER(SPECIES_WOBBUFFET); @@ -141,6 +144,7 @@ SINGLE_BATTLE_TEST("Keen Eye, Gen9+ Illuminate & Minds Eye don't prevent receivi PARAMETRIZE { species = SPECIES_URSALUNA_BLOODMOON; ability = ABILITY_MINDS_EYE; } GIVEN { + WITH_CONFIG(GEN_ILLUMINATE_EFFECT, GEN_9); ASSUME(GetMoveEffect(MOVE_BATON_PASS) == EFFECT_BATON_PASS); PLAYER(SPECIES_WOBBUFFET); OPPONENT(SPECIES_WOBBUFFET); @@ -173,6 +177,7 @@ SINGLE_BATTLE_TEST("Keen Eye & Gen9+ Illuminate don't prevent Spectral Thief fro PARAMETRIZE { species = SPECIES_STARYU; ability = ABILITY_ILLUMINATE; } GIVEN { + WITH_CONFIG(GEN_ILLUMINATE_EFFECT, GEN_9); ASSUME(GetMoveEffect(MOVE_HONE_CLAWS) == EFFECT_ATTACK_ACCURACY_UP); ASSUME(GetMoveEffect(MOVE_SPECTRAL_THIEF) == EFFECT_SPECTRAL_THIEF); PLAYER(SPECIES_WOBBUFFET); diff --git a/test/battle/ability/pickup.c b/test/battle/ability/pickup.c index 9db7c0f2de..cc9ae37e9d 100644 --- a/test/battle/ability/pickup.c +++ b/test/battle/ability/pickup.c @@ -23,10 +23,10 @@ SINGLE_BATTLE_TEST("Pickup grants an item used by another Pokémon") } } -WILD_BATTLE_TEST("Pickup grants an item used by itself in wild battles (Gen 9)") +WILD_BATTLE_TEST("Pickup grants an item used by itself in wild battles (Gen9+)") { GIVEN { - ASSUME(B_PICKUP_WILD >= GEN_9); + WITH_CONFIG(GEN_PICKUP_WILD, GEN_9); PLAYER(SPECIES_ZIGZAGOON) { Ability(ABILITY_PICKUP); MaxHP(100); HP(51); Item(ITEM_SITRUS_BERRY); } OPPONENT(SPECIES_WOBBUFFET); } WHEN { diff --git a/test/battle/ability/protean.c b/test/battle/ability/protean.c index 88144670a1..c5d141d244 100644 --- a/test/battle/ability/protean.c +++ b/test/battle/ability/protean.c @@ -1,14 +1,36 @@ #include "global.h" #include "test/battle.h" -ASSUMPTIONS -{ - ASSUME(B_PROTEAN_LIBERO == GEN_9); -} - -SINGLE_BATTLE_TEST("Protean changes the type of the user only once per switch in") +SINGLE_BATTLE_TEST("Protean changes the type of the user to the move used every time (Gen6-8)") { GIVEN { + WITH_CONFIG(GEN_PROTEAN_LIBERO, GEN_6); + PLAYER(SPECIES_REGIROCK); + OPPONENT(SPECIES_KECLEON) { Ability(ABILITY_PROTEAN); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_WATER_GUN); } + TURN { MOVE(opponent, MOVE_SCRATCH); } + TURN { SWITCH(opponent, 1); } + TURN { SWITCH(opponent, 0); } + TURN { MOVE(opponent, MOVE_WATER_GUN); } + } SCENE { + ABILITY_POPUP(opponent, ABILITY_PROTEAN); + MESSAGE("The opposing Kecleon transformed into the Water type!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_WATER_GUN, opponent); + ABILITY_POPUP(opponent, ABILITY_PROTEAN); + MESSAGE("The opposing Kecleon transformed into the Normal type!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponent); + ABILITY_POPUP(opponent, ABILITY_PROTEAN); + MESSAGE("The opposing Kecleon transformed into the Water type!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_WATER_GUN, opponent); + } +} + +SINGLE_BATTLE_TEST("Protean changes the type of the user only once per switch in (Gen9+)") +{ + GIVEN { + WITH_CONFIG(GEN_PROTEAN_LIBERO, GEN_9); PLAYER(SPECIES_REGIROCK); OPPONENT(SPECIES_KECLEON) { Ability(ABILITY_PROTEAN); } OPPONENT(SPECIES_WOBBUFFET); diff --git a/test/battle/ability/refrigerate.c b/test/battle/ability/refrigerate.c index 5f5092f93f..7b3af0277c 100644 --- a/test/battle/ability/refrigerate.c +++ b/test/battle/ability/refrigerate.c @@ -52,6 +52,7 @@ SINGLE_BATTLE_TEST("Refrigerate doesn't affect Weather Ball's type", s16 damage) PARAMETRIZE { move = MOVE_CELEBRATE; ability = ABILITY_REFRIGERATE; } PARAMETRIZE { move = MOVE_SUNNY_DAY; ability = ABILITY_REFRIGERATE; } GIVEN { + WITH_CONFIG(GEN_SNOW_WARNING, GEN_9); //To prevent capturing hail damage ASSUME(GetMoveEffect(MOVE_WEATHER_BALL) == EFFECT_WEATHER_BALL); ASSUME(gSpeciesInfo[SPECIES_PINSIR].types[0] == TYPE_BUG); PLAYER(SPECIES_AMAURA) { Ability(ability); } diff --git a/test/battle/ability/snow_warning.c b/test/battle/ability/snow_warning.c index 17f18814b8..171ad23b49 100644 --- a/test/battle/ability/snow_warning.c +++ b/test/battle/ability/snow_warning.c @@ -1,24 +1,30 @@ #include "global.h" #include "test/battle.h" -#if B_SNOW_WARNING < GEN_9 -SINGLE_BATTLE_TEST("Snow Warning summons hail") -#elif B_SNOW_WARNING >= GEN_9 -SINGLE_BATTLE_TEST("Snow Warning summons snow") -#endif +SINGLE_BATTLE_TEST("Snow Warning summons hail (Gen4-8)") { GIVEN { + WITH_CONFIG(GEN_SNOW_WARNING, GEN_8); PLAYER(SPECIES_ABOMASNOW) { Ability(ABILITY_SNOW_WARNING); } OPPONENT(SPECIES_WOBBUFFET); } WHEN { TURN {} } SCENE { - #if B_SNOW_WARNING < GEN_9 MESSAGE("It started to hail!"); ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HAIL_CONTINUES); - #elif B_SNOW_WARNING >= GEN_9 - MESSAGE("It started to snow!"); - ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_SNOW_CONTINUES); - #endif + } +} + +SINGLE_BATTLE_TEST("Snow Warning summons snow (Gen9+)") +{ + GIVEN { + WITH_CONFIG(GEN_SNOW_WARNING, GEN_9); + PLAYER(SPECIES_ABOMASNOW) { Ability(ABILITY_SNOW_WARNING); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN {} + } SCENE { + MESSAGE("It started to snow!"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_SNOW_CONTINUES); } } diff --git a/test/battle/ai/ai_switching.c b/test/battle/ai/ai_switching.c index 03334e04c2..75ff02cb88 100644 --- a/test/battle/ai/ai_switching.c +++ b/test/battle/ai/ai_switching.c @@ -842,6 +842,7 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI will switch out if it has an PARAMETRIZE { aiMon = SPECIES_SHIFTRY; absorbingAbility = ABILITY_WIND_RIDER; move = MOVE_HURRICANE;} GIVEN { ASSUME(B_REDIRECT_ABILITY_IMMUNITY >= GEN_5); + ASSUME(P_UPDATED_ABILITIES >= GEN_9); //For the predicted ability for Shiftry AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING); PLAYER(SPECIES_ZIGZAGOON) { Moves(move); } OPPONENT(SPECIES_ZIGZAGOON) { Moves(MOVE_SCRATCH); } diff --git a/test/battle/move_effect/ally_switch.c b/test/battle/move_effect/ally_switch.c index a8e9944e34..59844879d4 100644 --- a/test/battle/move_effect/ally_switch.c +++ b/test/battle/move_effect/ally_switch.c @@ -185,10 +185,25 @@ DOUBLE_BATTLE_TEST("Ally Switch doesn't make self-targeting status moves fail") } } -DOUBLE_BATTLE_TEST("Ally Switch increases the Protect-like moves counter") +DOUBLE_BATTLE_TEST("Ally Switch doesn't increase the Protect-like moves counter (Gen5-8)") { GIVEN { - ASSUME(B_ALLY_SWITCH_FAIL_CHANCE >= GEN_9); + WITH_CONFIG(GEN_ALLY_SWITCH_FAIL_CHANCE, GEN_8); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(playerLeft, MOVE_ALLY_SWITCH); } + } THEN { + EXPECT(gDisableStructs[B_POSITION_PLAYER_RIGHT].protectUses == 0); + } +} + +DOUBLE_BATTLE_TEST("Ally Switch increases the Protect-like moves counter (Gen9+)") +{ + GIVEN { + WITH_CONFIG(GEN_ALLY_SWITCH_FAIL_CHANCE, GEN_9); PLAYER(SPECIES_WOBBUFFET); PLAYER(SPECIES_WOBBUFFET); OPPONENT(SPECIES_WOBBUFFET); @@ -319,7 +334,7 @@ DOUBLE_BATTLE_TEST("Ally switch updates last used moves for Mimic") OPPONENT(SPECIES_FEAROW) { Speed(20); } OPPONENT(SPECIES_ARON) { Speed(30); } } WHEN { - TURN { MOVE(playerRight, MOVE_FAKE_OUT, target: opponentRight); MOVE(playerLeft, MOVE_ALLY_SWITCH); + TURN { MOVE(playerRight, MOVE_FAKE_OUT, target: opponentRight); MOVE(playerLeft, MOVE_ALLY_SWITCH); MOVE(opponentLeft, MOVE_MIMIC, target: playerLeft); } } SCENE { diff --git a/test/battle/move_effect/chloroblast.c b/test/battle/move_effect/chloroblast.c index 6452736478..d60449324f 100644 --- a/test/battle/move_effect/chloroblast.c +++ b/test/battle/move_effect/chloroblast.c @@ -137,9 +137,14 @@ SINGLE_BATTLE_TEST("Chloroblast is not affected by Reckless", s16 damage) u32 move; PARAMETRIZE { move = MOVE_CHLOROBLAST; } - PARAMETRIZE { move = MOVE_FRENZY_PLANT; } + if (B_UPDATED_MOVE_DATA >= GEN_9) { + PARAMETRIZE { move = MOVE_FRENZY_PLANT; } // 150 power + } else { + PARAMETRIZE { move = MOVE_SEED_FLARE; } // 120 power + } GIVEN { + ASSUME(GetMovePower(MOVE_CHLOROBLAST) == GetMovePower(move)); PLAYER(SPECIES_WOBBUFFET); OPPONENT(SPECIES_WOBBUFFET); } WHEN { diff --git a/test/battle/move_effect_secondary/steal_item.c b/test/battle/move_effect_secondary/steal_item.c index 7dd922e8b2..79c293757f 100644 --- a/test/battle/move_effect_secondary/steal_item.c +++ b/test/battle/move_effect_secondary/steal_item.c @@ -107,13 +107,13 @@ SINGLE_BATTLE_TEST("Thief and Covet don't steal target's held item if target has } // Test can't currently verify if the item is sent to Bag -WILD_BATTLE_TEST("Thief and Covet steal target's held item and it's added to Bag in wild battles (Gen 9)") +WILD_BATTLE_TEST("Thief and Covet steal target's held item and it's added to Bag in wild battles (Gen 9+)") { u32 move; PARAMETRIZE { move = MOVE_THIEF; } PARAMETRIZE { move = MOVE_COVET; } GIVEN { - ASSUME(B_STEAL_WILD_ITEMS >= GEN_9); + WITH_CONFIG(GEN_STEAL_WILD_ITEMS, GEN_9); PLAYER(SPECIES_WOBBUFFET); OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_HYPER_POTION); } } WHEN { From 80a3a3b7efcab7735a58eafe06bda417fb392bc6 Mon Sep 17 00:00:00 2001 From: cornixsenex <80906795+cornixsenex@users.noreply.github.com> Date: Fri, 8 Aug 2025 02:22:42 -0700 Subject: [PATCH 21/26] Remove shadow and hide follower on stair warp (#7368) --- include/field_effect.h | 1 + src/field_effect.c | 2 +- src/field_screen_effect.c | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/include/field_effect.h b/include/field_effect.h index 2e13879353..8a3de2b8de 100644 --- a/include/field_effect.h +++ b/include/field_effect.h @@ -37,6 +37,7 @@ bool8 FieldEffectCmd_loadgfx_callnative(u8 **script, u32 *val); bool8 FieldEffectCmd_loadtiles_callnative(u8 **script, u32 *val); bool8 FieldEffectCmd_loadfadedpal_callnative(u8 **script, u32 *val); void FieldCB_FallWarpExit(void); +void HideFollowerForFieldEffect(void); void StartEscalatorWarp(u8 metatileBehavior, u8 priority); void StartLavaridgeGymB1FWarp(u8 priority); void StartLavaridgeGym1FWarp(u8 priority); diff --git a/src/field_effect.c b/src/field_effect.c index 1f2feaba8f..82a5394821 100644 --- a/src/field_effect.c +++ b/src/field_effect.c @@ -1650,7 +1650,7 @@ static bool8 FallWarpEffect_End(struct Task *task) #define tState data[0] #define tGoingUp data[1] -static void HideFollowerForFieldEffect(void) +void HideFollowerForFieldEffect(void) { struct ObjectEvent *followerObj = GetFollowerObject(); if (!followerObj || followerObj->invisible) diff --git a/src/field_screen_effect.c b/src/field_screen_effect.c index 7b17f53666..0cf752fd6a 100644 --- a/src/field_screen_effect.c +++ b/src/field_screen_effect.c @@ -1571,12 +1571,14 @@ static void Task_ExitStairs(u8 taskId) tState++; break; } + gObjectEvents[gPlayerAvatar.objectEventId].noShadow = FALSE; } static void ForceStairsMovement(u32 metatileBehavior, s16 *speedX, s16 *speedY) { ObjectEventForceSetHeldMovement(&gObjectEvents[gPlayerAvatar.objectEventId], GetWalkInPlaceNormalMovementAction(GetPlayerFacingDirection())); GetStairsMovementDirection(metatileBehavior, speedX, speedY); + gObjectEvents[gPlayerAvatar.objectEventId].noShadow = TRUE; } #undef tSpeedX #undef tSpeedY @@ -1620,6 +1622,7 @@ static void Task_StairWarp(u8 taskId) LockPlayerFieldControls(); FreezeObjectEvents(); CameraObjectFreeze(); + HideFollowerForFieldEffect(); tState++; break; case 1: From 7de3663f383125c9288e87e9b8eda422705c5441 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Sun, 10 Aug 2025 16:58:36 +0200 Subject: [PATCH 22/26] Fixes Life Orb still activating if move was absorbed (#7521) --- include/battle.h | 3 +- src/battle_script_commands.c | 7 ++- src/battle_util.c | 6 +-- test/battle/hold_effect/life_orb.c | 50 +++++++++++++++++++ test/battle/move_effect/assurance.c | 27 ++++++++++ .../battle/move_effect_secondary/clear_smog.c | 19 +++++++ 6 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 test/battle/move_effect_secondary/clear_smog.c diff --git a/include/battle.h b/include/battle.h index 79cc591980..cd3126ba6b 100644 --- a/include/battle.h +++ b/include/battle.h @@ -169,7 +169,8 @@ struct ProtectStruct u16 activateOpportunist:2; // 2 - to copy stats. 1 - stats copied (do not repeat). 0 - no stats to copy u16 usedAllySwitch:1; u16 lashOutAffected:1; - u16 padding:4; + u16 assuranceDoubled:1; + u16 padding:3; // End of 16-bit bitfield u16 physicalDmg; u16 specialDmg; diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 0f3fbdba61..870f21d7a8 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -2272,6 +2272,9 @@ static void Cmd_adjustdamage(void) gBattleStruct->moveDamage[battlerDef] = gBattleMons[battlerDef].hp - 1; gSpecialStatuses[battlerDef].enduredDamage = TRUE; } + + if (gSpecialStatuses[battlerDef].enduredDamage) + gProtectStructs[battlerDef].assuranceDoubled = TRUE; } if (calcSpreadMoveDamage) @@ -2726,6 +2729,7 @@ static void Cmd_datahpupdate(void) gProtectStructs[battler].physicalBattlerId = gBattlerAttacker; else gProtectStructs[battler].physicalBattlerId = gBattlerTarget; + gProtectStructs[battler].assuranceDoubled = TRUE; } else if (!IsBattleMovePhysical(gCurrentMove) && !(gHitMarker & HITMARKER_PASSIVE_DAMAGE) && effect != EFFECT_PAIN_SPLIT) { @@ -2736,6 +2740,7 @@ static void Cmd_datahpupdate(void) gProtectStructs[battler].specialBattlerId = gBattlerAttacker; else gProtectStructs[battler].specialBattlerId = gBattlerTarget; + gProtectStructs[battler].assuranceDoubled = TRUE; } } gHitMarker &= ~HITMARKER_PASSIVE_DAMAGE; @@ -3722,7 +3727,7 @@ void SetMoveEffect(bool32 primary, bool32 certain) if (gBattleMons[gEffectBattler].statStages[i] != DEFAULT_STAT_STAGE) break; } - if ((gSpecialStatuses[gEffectBattler].physicalDmg || gSpecialStatuses[gEffectBattler].specialDmg) && i != NUM_BATTLE_STATS) + if (IsBattlerTurnDamaged(gEffectBattler) && i != NUM_BATTLE_STATS) { for (i = 0; i < NUM_BATTLE_STATS; i++) gBattleMons[gEffectBattler].statStages[i] = DEFAULT_STAT_STAGE; diff --git a/src/battle_util.c b/src/battle_util.c index 31c5229d0f..f8fb5faa86 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -7155,10 +7155,8 @@ u32 ItemBattleEffects(enum ItemCaseId caseID, u32 battler, bool32 moveTurn) case HOLD_EFFECT_LIFE_ORB: if (IsBattlerAlive(gBattlerAttacker) && !(gHitMarker & HITMARKER_UNABLE_TO_USE_MOVE) - && !IsBattleMoveStatus(gCurrentMove) - && (IsBattlerTurnDamaged(gBattlerTarget) || !(gBattleStruct->moveResultFlags[gBattlerTarget] & MOVE_RESULT_NO_EFFECT)) // Needs the second check in case of Substitute + && (IsBattlerTurnDamaged(gBattlerTarget) || gBattleScripting.savedDmg > 0) && GetBattlerAbility(gBattlerAttacker) != ABILITY_MAGIC_GUARD - && !gProtectStructs[gBattlerAttacker].confusionSelfDmg && !IsFutureSightAttackerInParty(gBattlerAttacker, gBattlerTarget, gCurrentMove)) { gBattleStruct->moveDamage[gBattlerAttacker] = GetNonDynamaxMaxHP(gBattlerAttacker) / 10; @@ -8157,7 +8155,7 @@ static inline u32 CalcMoveBasePower(struct DamageCalculationData *damageCalcData basePower = gBattleMons[battlerDef].hp * basePower / gBattleMons[battlerDef].maxHP; break; case EFFECT_ASSURANCE: - if (gProtectStructs[battlerDef].physicalDmg != 0 || gProtectStructs[battlerDef].specialDmg != 0 || gProtectStructs[battlerDef].confusionSelfDmg) + if (gProtectStructs[battlerDef].assuranceDoubled) basePower *= 2; break; case EFFECT_TRUMP_CARD: diff --git a/test/battle/hold_effect/life_orb.c b/test/battle/hold_effect/life_orb.c index 92c545dfad..2f1514538c 100644 --- a/test/battle/hold_effect/life_orb.c +++ b/test/battle/hold_effect/life_orb.c @@ -81,3 +81,53 @@ SINGLE_BATTLE_TEST("Life Orb doesn't cause any HP loss if user is unable to atta } } } + +SINGLE_BATTLE_TEST("Life Orb does not activate if on a confusion hit") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_LIFE_ORB); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_CONFUSE_RAY); MOVE(player, MOVE_POUND, WITH_RNG(RNG_CONFUSION, TRUE)); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_CONFUSE_RAY, opponent); + HP_BAR(player); + NONE_OF { + ANIMATION(ANIM_TYPE_MOVE, MOVE_POUND, player); + HP_BAR(opponent); + HP_BAR(player); + MESSAGE("Wobbuffet was hurt by the Life Orb!"); + } + } +} + +SINGLE_BATTLE_TEST("Life Orb does not activate if move was absorbed by target") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_LIFE_ORB); } + OPPONENT(SPECIES_RAICHU) { Ability(ABILITY_LIGHTNING_ROD); } + } WHEN { + TURN { MOVE(player, MOVE_SHOCK_WAVE); } + } SCENE { + NONE_OF { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SHOCK_WAVE, player); + HP_BAR(opponent); + HP_BAR(player); + MESSAGE("Wobbuffet was hurt by the Life Orb!"); + } + } +} + +SINGLE_BATTLE_TEST("Life Orb activates if move connected but no damage was dealt") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_LIFE_ORB); } + OPPONENT(SPECIES_WOBBUFFET) { HP(1); } + } WHEN { + TURN { MOVE(player, MOVE_FALSE_SWIPE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_FALSE_SWIPE, player); + HP_BAR(player); + MESSAGE("Wobbuffet was hurt by the Life Orb!"); + } +} diff --git a/test/battle/move_effect/assurance.c b/test/battle/move_effect/assurance.c index e6cbf72b9f..6eccb48f3f 100644 --- a/test/battle/move_effect/assurance.c +++ b/test/battle/move_effect/assurance.c @@ -6,3 +6,30 @@ TO_DO_BATTLE_TEST("Assurance doubles in power if the target has been damaged in TO_DO_BATTLE_TEST("Assurance doubles in power if the target has been damaged in the same turn - Crash"); TO_DO_BATTLE_TEST("Assurance doubles in power if the target has been damaged in the same turn - Confusion"); TO_DO_BATTLE_TEST("Assurance doubles in power if the target has been damaged in the same turn - Rocky Helmet"); + +DOUBLE_BATTLE_TEST("Assurance doubles in power if False Swipe connected but didn't do any damage") +{ + s16 hits[2]; + + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponentLeft, MOVE_ASSURANCE, target: playerRight); } + TURN { + MOVE(opponentLeft, MOVE_FALSE_SWIPE, target: playerLeft); + MOVE(playerLeft, MOVE_RECOVER); + MOVE(opponentRight, MOVE_ASSURANCE, target: playerLeft); + } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_ASSURANCE, opponentLeft); + HP_BAR(playerRight, captureDamage: &hits[0]); + ANIMATION(ANIM_TYPE_MOVE, MOVE_FALSE_SWIPE, opponentLeft); + ANIMATION(ANIM_TYPE_MOVE, MOVE_ASSURANCE, opponentRight); + HP_BAR(playerLeft, captureDamage: &hits[1]); + } THEN { + EXPECT_MUL_EQ(hits[0], Q_4_12(2.0), hits[1]); + } +} diff --git a/test/battle/move_effect_secondary/clear_smog.c b/test/battle/move_effect_secondary/clear_smog.c new file mode 100644 index 0000000000..f8d7b0eb2f --- /dev/null +++ b/test/battle/move_effect_secondary/clear_smog.c @@ -0,0 +1,19 @@ +#include "global.h" +#include "test/battle.h" + +SINGLE_BATTLE_TEST("Clear Smog removes stat changes even if it did no damage") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_SWORDS_DANCE); } + TURN { MOVE(player, MOVE_ENDURE); MOVE(opponent, MOVE_CLEAR_SMOG); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SWORDS_DANCE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_ENDURE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CLEAR_SMOG, opponent); + } THEN { + EXPECT_EQ(player->statStages[STAT_ATK], DEFAULT_STAT_STAGE); + } +} From cc736c423314c297dda2459f7f135db4146ddc02 Mon Sep 17 00:00:00 2001 From: Eduardo Quezada Date: Sun, 10 Aug 2025 16:56:42 -0400 Subject: [PATCH 23/26] Corrected Liquid Ooze test (#7527) --- test/battle/ability/liquid_ooze.c | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/test/battle/ability/liquid_ooze.c b/test/battle/ability/liquid_ooze.c index fa8ca8e3b2..1bd3cbbdf8 100644 --- a/test/battle/ability/liquid_ooze.c +++ b/test/battle/ability/liquid_ooze.c @@ -109,7 +109,7 @@ SINGLE_BATTLE_TEST("Liquid Ooze causes Strength Sap users to lose HP instead of } /* * https://bulbapedia.bulbagarden.net/wiki/Liquid_Ooze_(Ability)#In_battle: - * If the recipient of Leech Seed's effect were to faint due to Liquid Ooze on the same turn as the victim of Leech Seed, then the victim faints before the recipient. This means that the victim's team loses the battle if both teams had their final Pokémon sent out. + * If the recipient of Leech Seed's effect were to faint due to Liquid Ooze on the same turn as the victim of Leech Seed, then the victim faints before the recipient. This means that the victim's team loses the battle if both teams had their final Pokémon sent out. */ SINGLE_BATTLE_TEST("Liquid Ooze causes leech seed victim to faint before seeder") { @@ -140,6 +140,7 @@ SINGLE_BATTLE_TEST("Liquid Ooze causes leech seed victim to faint before seeder" SINGLE_BATTLE_TEST("Liquid Ooze causes Dream Eater users to lose HP instead of heal (Gen 5+)") { + KNOWN_FAILING; s16 damage; GIVEN { ASSUME(GetMoveEffect(MOVE_SPORE) == EFFECT_NON_VOLATILE_STATUS); @@ -158,8 +159,30 @@ SINGLE_BATTLE_TEST("Liquid Ooze causes Dream Eater users to lose HP instead of h HP_BAR(opponent); HP_BAR(player, captureDamage: &damage); } THEN { - EXPECT_LT(damage, 0); + EXPECT_GT(damage, 0); // Positive damage } } -TO_DO_BATTLE_TEST("Liquid Ooze does not cause Dream Eater users to lose HP instead of heal (Gen 3-4") +SINGLE_BATTLE_TEST("Liquid Ooze does not cause Dream Eater users to lose HP instead of heal (Gen 3-4") +{ + s16 damage; + GIVEN { + ASSUME(GetMoveEffect(MOVE_SPORE) == EFFECT_NON_VOLATILE_STATUS); + ASSUME(GetMoveNonVolatileStatus(MOVE_SPORE) == MOVE_EFFECT_SLEEP); + ASSUME(GetMoveEffect(MOVE_DREAM_EATER) == EFFECT_DREAM_EATER); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_TENTACRUEL) { Ability(ABILITY_LIQUID_OOZE); } + } WHEN { + TURN { MOVE(opponent, MOVE_SCRATCH); MOVE(player, MOVE_SPORE); } + TURN { MOVE(player, MOVE_DREAM_EATER); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponent); + HP_BAR(player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPORE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_DREAM_EATER, player); + HP_BAR(opponent); + HP_BAR(player, captureDamage: &damage); + } THEN { + EXPECT_LT(damage, 0); // Negative damage = Heal + } +} From de6c9c617692d6b6298fbfc8e722f1948ad8df23 Mon Sep 17 00:00:00 2001 From: Martin Griffin Date: Mon, 11 Aug 2025 09:06:23 +0100 Subject: [PATCH 24/26] Hacky support for enums in C asm statements (#7494) --- include/test/overworld_script.h | 1 - tools/preproc/asm_file.cpp | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/include/test/overworld_script.h b/include/test/overworld_script.h index e849f87e0c..a5bee797b2 100644 --- a/include/test/overworld_script.h +++ b/include/test/overworld_script.h @@ -51,7 +51,6 @@ asm(".set FALSE, 0\n" ".set VARS_END, " STR(VARS_END) "\n" ".set SPECIAL_VARS_START, " STR(SPECIAL_VARS_START) "\n" ".set SPECIAL_VARS_END, " STR(SPECIAL_VARS_END) "\n"); -asm(".include \"constants/gba_constants.inc\"\n"); // Make overworld script macros available. asm(".include \"constants/gba_constants.inc\"\n" diff --git a/tools/preproc/asm_file.cpp b/tools/preproc/asm_file.cpp index 83e84a6fde..10330f1774 100644 --- a/tools/preproc/asm_file.cpp +++ b/tools/preproc/asm_file.cpp @@ -620,6 +620,10 @@ bool AsmFile::ParseEnum() } enumCounter = 0; } + // HACK(#7394): Make the definitions global so that C 'asm' + // statements are able to reference them (if they happen to + // be available in an assembled object file). + std::printf(".global %s; ", currentIdentName.c_str()); std::printf(".equiv %s, (%s) + %ld\n", currentIdentName.c_str(), enumBase.c_str(), enumCounter); enumCounter++; symbolCount++; From a9d5c2cd289e6ad4aca2f541982da5fb940a7e6f Mon Sep 17 00:00:00 2001 From: Eduardo Quezada Date: Mon, 11 Aug 2025 10:06:58 -0400 Subject: [PATCH 25/26] Updated Dream Eater/Liquid Ooze's interaction to Gen 5 standards (#7528) --- data/battle_scripts_1.s | 51 +++--------------------- docs/tutorials/how_to_testing_system.md | 2 +- include/battle_scripts.h | 2 +- include/config/battle.h | 3 +- include/constants/battle_move_effects.h | 2 +- include/constants/battle_string_ids.h | 1 - include/constants/generational_changes.h | 1 + include/generational_changes.h | 1 + include/test/battle.h | 2 +- src/battle_message.c | 1 - src/battle_script_commands.c | 14 ++++--- src/battle_util.c | 4 +- test/battle/ability/liquid_ooze.c | 3 +- test/battle/move_effect/dream_eater.c | 4 +- 14 files changed, 27 insertions(+), 64 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 53f6697aef..248d79714d 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -2835,13 +2835,6 @@ BattleScript_AlreadyAsleep:: setmoveresultflags MOVE_RESULT_FAILED goto BattleScript_MoveEnd -BattleScript_WasntAffected:: - pause B_WAIT_TIME_SHORT - printstring STRINGID_PKMNWASNTAFFECTED - waitmessage B_WAIT_TIME_LONG - setmoveresultflags MOVE_RESULT_FAILED - goto BattleScript_MoveEnd - BattleScript_CantMakeAsleep:: pause B_WAIT_TIME_SHORT printfromtable gUproarAwakeStringIds @@ -2886,44 +2879,10 @@ BattleScript_MaxHp50Recoil:: BattleScript_EffectDreamEater:: attackcanceler - jumpifsubstituteblocks BattleScript_DreamEaterNoEffect - jumpifstatus BS_TARGET, STATUS1_SLEEP, BattleScript_DreamEaterWorked - jumpifability BS_TARGET, ABILITY_COMATOSE, BattleScript_DreamEaterWorked -BattleScript_DreamEaterNoEffect: - attackstring - ppreduce - waitmessage B_WAIT_TIME_LONG - goto BattleScript_WasntAffected -BattleScript_DreamEaterWorked: - accuracycheck BattleScript_PrintMoveMissed, ACC_CURR_MOVE - attackstring - ppreduce - critcalc - damagecalc - adjustdamage - attackanimation - waitanimation - effectivenesssound - hitanimation BS_TARGET - waitstate - healthbarupdate BS_TARGET - datahpupdate BS_TARGET - critmessage - waitmessage B_WAIT_TIME_LONG - resultmessage - waitmessage B_WAIT_TIME_LONG - jumpifstatus3 BS_ATTACKER, STATUS3_HEAL_BLOCK, BattleScript_DreamEaterTryFaintEnd - setdrainedhp - manipulatedamage DMG_BIG_ROOT - orword gHitMarker, HITMARKER_IGNORE_SUBSTITUTE - healthbarupdate BS_ATTACKER - datahpupdate BS_ATTACKER - jumpifmovehadnoeffect BattleScript_DreamEaterTryFaintEnd - printstring STRINGID_PKMNENERGYDRAINED - waitmessage B_WAIT_TIME_LONG -BattleScript_DreamEaterTryFaintEnd: - tryfaintmon BS_TARGET - goto BattleScript_MoveEnd + jumpifsubstituteblocks BattleScript_DoesntAffectTargetAtkString + jumpifstatus BS_TARGET, STATUS1_SLEEP, BattleScript_HitFromAccCheck + jumpifability BS_TARGET, ABILITY_COMATOSE, BattleScript_HitFromAccCheck + goto BattleScript_DoesntAffectTargetAtkString BattleScript_EffectMirrorMove:: attackcanceler @@ -9168,7 +9127,7 @@ BattleScript_EjectPackActivates:: jumpifcantswitch BS_SCRIPTING, BattleScript_EjectButtonEnd goto BattleScript_EjectPackActivate_Ret -BattleScript_DarkTypePreventsPrankster:: +BattleScript_DoesntAffectTargetAtkString:: attackstring ppreduce pause B_WAIT_TIME_SHORT diff --git a/docs/tutorials/how_to_testing_system.md b/docs/tutorials/how_to_testing_system.md index c5fbb945aa..b663ad54b4 100644 --- a/docs/tutorials/how_to_testing_system.md +++ b/docs/tutorials/how_to_testing_system.md @@ -428,7 +428,7 @@ Spaces in pattern match newlines (\n, \l, and \p) in the message. Often used to check that a battler took its turn but it failed, e.g.: ``` MESSAGE("Wobbuffet used Dream Eater!"); - MESSAGE("The opposing Wobbuffet wasn't affected!"); + MESSAGE("It doesn't affect the opposing Wobbuffet…"); ``` ### `STATUS_ICON` diff --git a/include/battle_scripts.h b/include/battle_scripts.h index ef6206cf45..46326a42df 100644 --- a/include/battle_scripts.h +++ b/include/battle_scripts.h @@ -426,7 +426,7 @@ extern const u8 BattleScript_MentalHerbCureEnd2[]; extern const u8 BattleScript_TerrainPreventsEnd2[]; extern const u8 BattleScript_MistyTerrainPrevents[]; extern const u8 BattleScript_ElectricTerrainPrevents[]; -extern const u8 BattleScript_DarkTypePreventsPrankster[]; +extern const u8 BattleScript_DoesntAffectTargetAtkString[]; extern const u8 BattleScript_GulpMissileGorging[]; extern const u8 BattleScript_GulpMissileGulping[]; extern const u8 BattleScript_GulpMissileFormChange[]; diff --git a/include/config/battle.h b/include/config/battle.h index d09698c634..3cfd319304 100644 --- a/include/config/battle.h +++ b/include/config/battle.h @@ -131,7 +131,8 @@ #define B_SKIP_RECHARGE GEN_LATEST // In Gen1, recharging moves such as Hyper Beam skip the recharge if the target gets KO'd #define B_ENCORE_TARGET GEN_LATEST // In Gen5+, encored moves are allowed to choose a target #define B_TIME_OF_DAY_HEALING_MOVES GEN_LATEST // In Gen2, Morning Sun, Moonlight, and Synthesis heal twice as much HP based off the time of day. Also changes how much they heal. Evening affects Moonlight. - // If OW_TIMES_OF_DAY is set to Gen 3, then Morning Sun is boosted during the day. + // If OW_TIMES_OF_DAY is set to Gen 3, then Morning Sun is boosted during the day. +#define B_DREAM_EATER_LIQUID_OOZE GEN_LATEST // In Gen5+, Dream Eater is affected by Liquid Ooze. // Ability settings #define B_GALE_WINGS GEN_LATEST // In Gen7+ requires full HP to trigger. diff --git a/include/constants/battle_move_effects.h b/include/constants/battle_move_effects.h index 53c9843d5b..2ddb75adf6 100644 --- a/include/constants/battle_move_effects.h +++ b/include/constants/battle_move_effects.h @@ -9,7 +9,7 @@ enum __attribute__((packed)) BattleMoveEffects EFFECT_ABSORB, EFFECT_EXPLOSION, EFFECT_MISTY_EXPLOSION, // Same as EFFECT_EXPLOSION but it's boosted on Misty Terrain - EFFECT_DREAM_EATER, + EFFECT_DREAM_EATER, // Same as EFFECT_ABSORB but it can only be used on sleeping targets EFFECT_MIRROR_MOVE, EFFECT_ATTACK_UP, EFFECT_DEFENSE_UP, diff --git a/include/constants/battle_string_ids.h b/include/constants/battle_string_ids.h index df7b5e6232..3343a449e7 100644 --- a/include/constants/battle_string_ids.h +++ b/include/constants/battle_string_ids.h @@ -41,7 +41,6 @@ enum StringID STRINGID_PKMNMADESLEEP, STRINGID_PKMNALREADYASLEEP, STRINGID_PKMNALREADYASLEEP2, - STRINGID_PKMNWASNTAFFECTED, STRINGID_PKMNWASPOISONED, STRINGID_PKMNPOISONEDBY, STRINGID_PKMNHURTBYPOISON, diff --git a/include/constants/generational_changes.h b/include/constants/generational_changes.h index 6892f70ee2..e51d57915c 100644 --- a/include/constants/generational_changes.h +++ b/include/constants/generational_changes.h @@ -28,6 +28,7 @@ enum GenConfigTag GEN_STEAL_WILD_ITEMS, GEN_SNOW_WARNING, GEN_ALLY_SWITCH_FAIL_CHANCE, + GEN_DREAM_EATER_LIQUID_OOZE, GEN_CONFIG_COUNT }; diff --git a/include/generational_changes.h b/include/generational_changes.h index 9c24608e63..53991bab1f 100644 --- a/include/generational_changes.h +++ b/include/generational_changes.h @@ -31,6 +31,7 @@ static const u8 sGenerationalChanges[GEN_CONFIG_COUNT] = [GEN_STEAL_WILD_ITEMS] = B_STEAL_WILD_ITEMS, [GEN_SNOW_WARNING] = B_SNOW_WARNING, [GEN_ALLY_SWITCH_FAIL_CHANCE] = B_ALLY_SWITCH_FAIL_CHANCE, + [GEN_DREAM_EATER_LIQUID_OOZE] = B_DREAM_EATER_LIQUID_OOZE, }; #if TESTING diff --git a/include/test/battle.h b/include/test/battle.h index 2ec2b1da0a..b95038b09e 100644 --- a/include/test/battle.h +++ b/include/test/battle.h @@ -434,7 +434,7 @@ * Spaces in pattern match newlines (\n, \l, and \p) in the message. * Often used to check that a battler took its turn but it failed, e.g.: * MESSAGE("Wobbuffet used Dream Eater!"); - * MESSAGE("The opposing Wobbuffet wasn't affected!"); + * MESSAGE("It doesn't affect the opposing Wobbuffet…"); * * STATUS_ICON(battler, status1 | none: | sleep: | poison: | burn: | freeze: | paralysis:, badPoison:) * Causes the test to fail if the battler's status is not changed to the diff --git a/src/battle_message.c b/src/battle_message.c index d49b96b8be..132cfa66f0 100644 --- a/src/battle_message.c +++ b/src/battle_message.c @@ -200,7 +200,6 @@ const u8 *const gBattleStringsTable[STRINGID_COUNT] = [STRINGID_PKMNMADESLEEP] = COMPOUND_STRING("{B_SCR_NAME_WITH_PREFIX}'s {B_BUFF1} made {B_EFF_NAME_WITH_PREFIX2} sleep!"), //not in gen 5+, ability popup [STRINGID_PKMNALREADYASLEEP] = COMPOUND_STRING("{B_DEF_NAME_WITH_PREFIX} is already asleep!"), [STRINGID_PKMNALREADYASLEEP2] = COMPOUND_STRING("{B_ATK_NAME_WITH_PREFIX} is already asleep!"), - [STRINGID_PKMNWASNTAFFECTED] = COMPOUND_STRING("{B_DEF_NAME_WITH_PREFIX} wasn't affected!"), //not in gen 5+, ability popup [STRINGID_PKMNWASPOISONED] = COMPOUND_STRING("{B_EFF_NAME_WITH_PREFIX} was poisoned!"), [STRINGID_PKMNPOISONEDBY] = COMPOUND_STRING("{B_EFF_NAME_WITH_PREFIX} was poisoned by {B_SCR_NAME_WITH_PREFIX2}'s {B_BUFF1}!"), //not in gen 5+, ability popup [STRINGID_PKMNHURTBYPOISON] = COMPOUND_STRING("{B_ATK_NAME_WITH_PREFIX} was hurt by its poisoning!"), diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 109f044f2c..8bd6d3ab6a 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -6066,6 +6066,7 @@ static void Cmd_moveend(void) switch (moveEffect) { case EFFECT_ABSORB: + case EFFECT_DREAM_EATER: if (!(gStatuses3[gBattlerAttacker] & STATUS3_HEAL_BLOCK) && gBattleStruct->moveDamage[gBattlerTarget] > 0 && IsBattlerAlive(gBattlerAttacker)) @@ -6075,18 +6076,19 @@ static void Cmd_moveend(void) gHitMarker |= HITMARKER_IGNORE_SUBSTITUTE | HITMARKER_IGNORE_DISGUISE; effect = TRUE; - if (GetBattlerAbility(gBattlerTarget) == ABILITY_LIQUID_OOZE) + if ((moveEffect == EFFECT_DREAM_EATER && GetGenConfig(GEN_DREAM_EATER_LIQUID_OOZE) < GEN_5) + || GetBattlerAbility(gBattlerTarget) != ABILITY_LIQUID_OOZE) + { + gBattleCommunication[MULTISTRING_CHOOSER] = B_MSG_ABSORB; + BattleScriptCall(BattleScript_EffectAbsorb); + } + else { gBattleStruct->moveDamage[gBattlerAttacker] *= -1; gHitMarker |= HITMARKER_PASSIVE_DAMAGE; gBattleCommunication[MULTISTRING_CHOOSER] = B_MSG_ABSORB_OOZE; BattleScriptCall(BattleScript_EffectAbsorbLiquidOoze); } - else - { - gBattleCommunication[MULTISTRING_CHOOSER] = B_MSG_ABSORB; - BattleScriptCall(BattleScript_EffectAbsorb); - } } break; case EFFECT_FINAL_GAMBIT: diff --git a/src/battle_util.c b/src/battle_util.c index 97753232c6..13f830d953 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -1194,7 +1194,7 @@ void PrepareStringBattle(enum StringID stringId, u32 battler) SET_STATCHANGER(STAT_SPEED, 1, FALSE); } - if ((stringId == STRINGID_ITDOESNTAFFECT || stringId == STRINGID_PKMNWASNTAFFECTED || stringId == STRINGID_PKMNUNAFFECTED)) + if ((stringId == STRINGID_ITDOESNTAFFECT || stringId == STRINGID_PKMNUNAFFECTED)) TryInitializeTrainerSlideEnemyMonUnaffected(gBattlerTarget); BtlController_EmitPrintString(battler, B_COMM_TO_CONTROLLER, stringId); @@ -3046,7 +3046,7 @@ bool32 CanAbilityBlockMove(u32 battlerAtk, u32 battlerDef, u32 abilityAtk, u32 a if (option == RUN_SCRIPT && !IsSpreadMove(GetBattlerMoveTargetType(battlerAtk, move))) CancelMultiTurnMoves(battlerAtk, SKY_DROP_ATTACKCANCELLER_CHECK); // Don't cancel moves that can hit two targets bc one target might not be protected - battleScriptBlocksMove = BattleScript_DarkTypePreventsPrankster; + battleScriptBlocksMove = BattleScript_DoesntAffectTargetAtkString; } // Check def partner ability diff --git a/test/battle/ability/liquid_ooze.c b/test/battle/ability/liquid_ooze.c index 1bd3cbbdf8..2c89702911 100644 --- a/test/battle/ability/liquid_ooze.c +++ b/test/battle/ability/liquid_ooze.c @@ -140,9 +140,9 @@ SINGLE_BATTLE_TEST("Liquid Ooze causes leech seed victim to faint before seeder" SINGLE_BATTLE_TEST("Liquid Ooze causes Dream Eater users to lose HP instead of heal (Gen 5+)") { - KNOWN_FAILING; s16 damage; GIVEN { + WITH_CONFIG(GEN_DREAM_EATER_LIQUID_OOZE, GEN_5); ASSUME(GetMoveEffect(MOVE_SPORE) == EFFECT_NON_VOLATILE_STATUS); ASSUME(GetMoveNonVolatileStatus(MOVE_SPORE) == MOVE_EFFECT_SLEEP); ASSUME(GetMoveEffect(MOVE_DREAM_EATER) == EFFECT_DREAM_EATER); @@ -167,6 +167,7 @@ SINGLE_BATTLE_TEST("Liquid Ooze does not cause Dream Eater users to lose HP inst { s16 damage; GIVEN { + WITH_CONFIG(GEN_DREAM_EATER_LIQUID_OOZE, GEN_3); ASSUME(GetMoveEffect(MOVE_SPORE) == EFFECT_NON_VOLATILE_STATUS); ASSUME(GetMoveNonVolatileStatus(MOVE_SPORE) == MOVE_EFFECT_SLEEP); ASSUME(GetMoveEffect(MOVE_DREAM_EATER) == EFFECT_DREAM_EATER); diff --git a/test/battle/move_effect/dream_eater.c b/test/battle/move_effect/dream_eater.c index caa365a17a..baefd2e4ff 100644 --- a/test/battle/move_effect/dream_eater.c +++ b/test/battle/move_effect/dream_eater.c @@ -33,7 +33,7 @@ SINGLE_BATTLE_TEST("Dream Eater fails on awake targets") TURN { MOVE(player, MOVE_DREAM_EATER); } } SCENE { MESSAGE("Wobbuffet used Dream Eater!"); - MESSAGE("The opposing Wobbuffet wasn't affected!"); + MESSAGE("It doesn't affect the opposing Wobbuffet…"); } } @@ -88,7 +88,7 @@ SINGLE_BATTLE_TEST("Dream Eater fails if the target is behind a Substitute (Gen TURN { MOVE(opponent, MOVE_DREAM_EATER); } } SCENE { MESSAGE("The opposing Wobbuffet used Dream Eater!"); - MESSAGE("Wobbuffet wasn't affected!"); + MESSAGE("It doesn't affect Wobbuffet…"); } } #else From 38cfc4bacf3eb4dabb22c3a3e8b81abbb27840d1 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:26:41 +0200 Subject: [PATCH 26/26] Modify move prediction to work in doubles (#7087) --- include/battle.h | 8 +-- include/battle_ai_util.h | 2 +- include/config/ai.h | 4 +- src/battle_ai_main.c | 108 +++++++++++++--------------- src/battle_ai_switch_items.c | 34 ++++----- src/battle_ai_util.c | 20 +++--- test/battle/ai/ai_check_viability.c | 24 +++++++ 7 files changed, 107 insertions(+), 93 deletions(-) diff --git a/include/battle.h b/include/battle.h index 47739ca264..086eb68f51 100644 --- a/include/battle.h +++ b/include/battle.h @@ -329,12 +329,12 @@ struct AiLogicData u8 ejectButtonSwitch:1; // Tracks whether current switch out was from Eject Button u8 ejectPackSwitch:1; // Tracks whether current switch out was from Eject Pack u8 predictingSwitch:1; // Determines whether AI will use switch predictions this turn or not - u8 predictingMove:1; // Determines whether AI will use move predictions this turn or not u8 aiPredictionInProgress:1; // Tracks whether the AI is in the middle of running prediction calculations - u8 padding:2; - u8 shouldSwitch; // Stores result of ShouldSwitch, which decides whether a mon should be switched out u8 aiCalcInProgress:1; - u8 battlerDoingPrediction; // Stores which battler is currently running its prediction calcs + u8 predictingMove:1; // Determines whether AI will use move predictions this turn or not + u8 padding1:1; + u8 shouldSwitch:4; // Stores result of ShouldSwitch, which decides whether a mon should be switched out + u8 padding2:4; u16 predictedMove[MAX_BATTLERS_COUNT]; }; diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 448fbf71c5..118fbf5984 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -80,6 +80,7 @@ u32 AI_GetDamage(u32 battlerAtk, u32 battlerDef, u32 moveIndex, enum DamageCalcC bool32 IsAiVsAiBattle(void); bool32 BattlerHasAi(u32 battlerId); bool32 IsAiBattlerAware(u32 battlerId); +bool32 CanAiPredictMove(void); bool32 IsAiBattlerAssumingStab(void); bool32 IsAiBattlerAssumingStatusMoves(void); bool32 ShouldRecordStatusMove(u32 move); @@ -297,7 +298,6 @@ bool32 IsBattlerPredictedToSwitch(u32 battler); u32 GetIncomingMove(u32 battler, u32 opposingBattler, struct AiLogicData *aiData); bool32 HasLowAccuracyMove(u32 battlerAtk, u32 battlerDef); bool32 HasBattlerSideAbility(u32 battlerDef, u32 ability, struct AiLogicData *aiData); -u32 GetThinkingBattler(u32 battler); bool32 IsNaturalEnemy(u32 speciesAttacker, u32 speciesTarget); // These are for the purpose of not doubling up on moves during double battles. diff --git a/include/config/ai.h b/include/config/ai.h index 8d015ce9e4..b6eb7bfe21 100644 --- a/include/config/ai.h +++ b/include/config/ai.h @@ -52,7 +52,7 @@ #define AI_GOOD_SCORE_THRESHOLD 100 // Move scores above this threshold are considered "good" when deciding switching // AI held item-based move scoring -#define LOW_ACCURACY_THRESHOLD 75 // Moves with accuracy equal OR below this value are considered low accuracy +#define LOW_ACCURACY_THRESHOLD 75 // Moves with accuracy equal OR below this value are considered low accuracy // AI move scoring #define STATUS_MOVE_FOCUS_PUNCH_CHANCE 50 // Chance the AI will use a status move if the player's best move is Focus Punch @@ -73,7 +73,7 @@ // AI Terastalization chances #define AI_CONSERVE_TERA_CHANCE_PER_MON 10 // Chance for AI with smart tera flag to decide not to tera before considering defensive benefit is this*(X-1), where X is the number of alive pokemon that could tera -#define AI_TERA_PREDICT_CHANCE 40 // Chance for AI with smart tera flag to tera in the situation where tera would save it from a KO, but could be punished by a KO from a different move. +#define AI_TERA_PREDICT_CHANCE 40 // Chance for AI with smart tera flag to tera in the situation where tera would save it from a KO, but could be punished by a KO from a different move. // AI_FLAG_PP_STALL_PREVENTION settings #define PP_STALL_DISREGARD_MOVE_PERCENTAGE 50 // Detection chance per roll diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 5e75f1dfb3..5460e31dee 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -33,6 +33,7 @@ #define AI_ACTION_WATCH (1 << 2) #define AI_ACTION_DO_NOT_ATTACK (1 << 3) +static u32 ChooseMoveOrAction(u32 battler); static u32 ChooseMoveOrAction_Singles(u32 battler); static u32 ChooseMoveOrAction_Doubles(u32 battler); static inline void BattleAI_DoAIProcessing(struct AiThinkingStruct *aiThink, u32 battlerAtk, u32 battlerDef); @@ -278,16 +279,17 @@ void BattleAI_SetupFlags(void) { gAiThinkingStruct->aiFlags[B_POSITION_PLAYER_RIGHT] = gAiThinkingStruct->aiFlags[B_POSITION_PLAYER_LEFT]; } - else + else // Assign ai flags for player for prediction { - gAiThinkingStruct->aiFlags[B_POSITION_PLAYER_RIGHT] = 0; // player + u64 aiFlags = GetAiFlags(TRAINER_BATTLE_PARAM.opponentA) | GetAiFlags(TRAINER_BATTLE_PARAM.opponentB); + gAiThinkingStruct->aiFlags[B_POSITION_PLAYER_RIGHT] = aiFlags; + gAiThinkingStruct->aiFlags[B_POSITION_PLAYER_LEFT] = aiFlags; } } void BattleAI_SetupAIData(u8 defaultScoreMoves, u32 battler) { - u32 moveLimitations, moveLimitationsTarget; - u32 defaultScoreMovesTarget = defaultScoreMoves; + u32 moveLimitations; u64 flags[MAX_BATTLERS_COUNT]; u32 moveIndex; @@ -313,25 +315,6 @@ void BattleAI_SetupAIData(u8 defaultScoreMoves, u32 battler) gBattlerTarget = SetRandomTarget(battler); gAiBattleData->chosenTarget[battler] = gBattlerTarget; - - // Initialize move prediction scores - if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_PREDICT_MOVE) - { - u32 opposingBattler = GetOppositeBattler(battler); - moveLimitationsTarget = gAiLogicData->moveLimitations[opposingBattler]; - - for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++) - { - if (moveLimitationsTarget & (1u << moveIndex)) - SET_SCORE(opposingBattler, moveIndex, 0); - if (defaultScoreMovesTarget & 1) - SET_SCORE(opposingBattler, moveIndex, AI_SCORE_DEFAULT); - else - SET_SCORE(opposingBattler, moveIndex, 0); - - defaultScoreMovesTarget >>= 1; - } - } } bool32 BattlerChoseNonMoveAction(void) @@ -355,7 +338,6 @@ void SetupAIPredictionData(u32 battler, enum SwitchType switchType) { s32 opposingBattler = GetOppositeBattler(battler); gAiLogicData->aiPredictionInProgress = TRUE; - gAiLogicData->battlerDoingPrediction = battler; // Switch prediction if ((gAiThinkingStruct->aiFlags[battler] & AI_FLAG_PREDICT_SWITCH)) @@ -364,31 +346,21 @@ void SetupAIPredictionData(u32 battler, enum SwitchType switchType) if (ShouldSwitch(opposingBattler)) gAiLogicData->shouldSwitch |= (1u << opposingBattler); gBattleStruct->prevTurnSpecies[opposingBattler] = gBattleMons[opposingBattler].species; - - // Determine whether AI will use predictions this turn - gAiLogicData->predictingSwitch = RandomPercentage(RNG_AI_PREDICT_SWITCH, PREDICT_SWITCH_CHANCE); } - // Move prediction - if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_PREDICT_MOVE) - { - gAiLogicData->predictedMove[opposingBattler] = gBattleMons[opposingBattler].moves[BattleAI_ChooseMoveIndex(opposingBattler)]; - ModifySwitchAfterMoveScoring(opposingBattler); + // Determine whether AI will use predictions this turn + gAiLogicData->predictingSwitch = RandomPercentage(RNG_AI_PREDICT_SWITCH, PREDICT_SWITCH_CHANCE); - // Determine whether AI will use predictions this turn - gAiLogicData->predictingMove = RandomPercentage(RNG_AI_PREDICT_MOVE, PREDICT_MOVE_CHANCE); - } gAiLogicData->aiPredictionInProgress = FALSE; } void ComputeBattlerDecisions(u32 battler) { - if ((gBattleTypeFlags & BATTLE_TYPE_HAS_AI || IsWildMonSmart()) - && (BattlerHasAi(battler) - && !(gBattleTypeFlags & BATTLE_TYPE_PALACE))) + bool32 isAiBattler = (gBattleTypeFlags & BATTLE_TYPE_HAS_AI || IsWildMonSmart()) && (BattlerHasAi(battler) && !(gBattleTypeFlags & BATTLE_TYPE_PALACE)); + if (isAiBattler || CanAiPredictMove()) { // If ai is about to flee or chosen to watch player, no need to calc anything - if (BattlerChoseNonMoveAction()) + if (isAiBattler && BattlerChoseNonMoveAction()) return; // Risky AI switches aggressively even mid battle @@ -401,10 +373,13 @@ void ComputeBattlerDecisions(u32 battler) SetupAIPredictionData(battler, switchType); // AI's own switching data - gAiLogicData->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, switchType); - if (ShouldSwitch(battler)) - gAiLogicData->shouldSwitch |= (1u << battler); - gBattleStruct->prevTurnSpecies[battler] = gBattleMons[battler].species; + if (isAiBattler) + { + gAiLogicData->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, switchType); + if (ShouldSwitch(battler)) + gAiLogicData->shouldSwitch |= (1u << battler); + gBattleStruct->prevTurnSpecies[battler] = gBattleMons[battler].species; + } // AI's move scoring gAiBattleData->chosenMoveIndex[battler] = BattleAI_ChooseMoveIndex(battler); // Calculate score and chose move index @@ -425,6 +400,13 @@ void ReconsiderGimmick(u32 battlerAtk, u32 battlerDef, u16 move) SetAIUsingGimmick(battlerAtk, NO_GIMMICK); } +static u32 ChooseMoveOrAction(u32 battler) +{ + if (IsDoubleBattle()) + return ChooseMoveOrAction_Doubles(battler); + return ChooseMoveOrAction_Singles(battler); +} + u32 BattleAI_ChooseMoveIndex(u32 battler) { u32 chosenMoveIndex; @@ -434,16 +416,11 @@ u32 BattleAI_ChooseMoveIndex(u32 battler) if (gBattleStruct->gimmick.usableGimmick[battler] == GIMMICK_TERA && (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_TERA)) DecideTerastal(battler); - - if (!IsDoubleBattle()) - chosenMoveIndex = ChooseMoveOrAction_Singles(battler); - else - chosenMoveIndex = ChooseMoveOrAction_Doubles(battler); + chosenMoveIndex = ChooseMoveOrAction(battler); if (gBattleStruct->gimmick.usableGimmick[battler] != GIMMICK_NONE) ReconsiderGimmick(battler, gBattlerTarget, gBattleMons[battler].moves[chosenMoveIndex]); - // Clear protect structures, some flags may be set during AI calcs // e.g. pranksterElevated from GetBattleMovePriority memset(&gProtectStructs, 0, MAX_BATTLERS_COUNT * sizeof(struct ProtectStruct)); @@ -573,7 +550,6 @@ void RecordStatusMoves(u32 battler) void SetBattlerAiData(u32 battler, struct AiLogicData *aiData) { u32 ability, holdEffect; - ability = aiData->abilities[battler] = AI_DecideKnownAbilityForTurn(battler); aiData->items[battler] = gBattleMons[battler].item; holdEffect = aiData->holdEffects[battler] = AI_DecideHoldEffectForTurn(battler); @@ -691,6 +667,20 @@ void SetAiLogicDataForTurn(struct AiLogicData *aiData) SetBattlerAiMovesData(aiData, battlerAtk, battlersCount, weather); } + + for (battlerAtk = 0; battlerAtk < battlersCount; battlerAtk++) + { + // Prediction limited to player side but can be expanded to read partners move in the future + if (!IsOnPlayerSide(battlerAtk) || !CanAiPredictMove()) + continue; + + // This can potentially be cleaned up more + BattleAI_SetupAIData(0xF, battlerAtk); + u32 chosenMoveIndex = ChooseMoveOrAction(battlerAtk); + gAiLogicData->predictedMove[battlerAtk] = gBattleMons[battlerAtk].moves[chosenMoveIndex]; + aiData->predictingMove = RandomPercentage(RNG_AI_PREDICT_MOVE, PREDICT_MOVE_CHANCE); + } + if (DEBUG_AI_DELAY_TIMER) // We add to existing to compound multiple calls gBattleStruct->aiDelayCycles += CycleCountEnd(); @@ -749,7 +739,7 @@ static u32 ChooseMoveOrAction_Singles(u32 battler) u8 consideredMoveArray[MAX_MON_MOVES]; u32 numOfBestMoves; s32 i; - u64 flags = gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)]; + u64 flags = gAiThinkingStruct->aiFlags[battler]; u32 opposingBattler = GetOppositeBattler(battler); gAiLogicData->partnerMove = 0; // no ally @@ -831,7 +821,7 @@ static u32 ChooseMoveOrAction_Doubles(u32 battler) gAiLogicData->partnerMove = GetAllyChosenMove(battler); gAiThinkingStruct->aiLogicId = 0; gAiThinkingStruct->movesetIndex = 0; - flags = gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)]; + flags = gAiThinkingStruct->aiFlags[battler]; while (flags != 0) { @@ -1139,7 +1129,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) abilityDef = ABILITY_NONE; // If a pokemon can be guaranteed flinched, don't target the pokemon that can't be flinched. - if (hasTwoOpponents + if (hasTwoOpponents && !IsFlinchGuaranteed(battlerAtk, battlerDef, move) && IsFlinchGuaranteed(battlerAtk, BATTLE_PARTNER(battlerDef), move) && aiData->effectiveness[battlerAtk][BATTLE_PARTNER(battlerDef)][gAiThinkingStruct->movesetIndex] != UQ_4_12(0.0)) ADJUST_SCORE(-5); @@ -1975,7 +1965,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_CHILLY_RECEPTION: if (CountUsablePartyMons(battlerAtk) == 0) ADJUST_SCORE(-10); - else if (weather & (B_WEATHER_ICY_ANY | B_WEATHER_PRIMAL_ANY) + else if (weather & (B_WEATHER_ICY_ANY | B_WEATHER_PRIMAL_ANY) || (HasPartner(battlerAtk) && AreMovesEquivalent(battlerAtk, BATTLE_PARTNER(battlerAtk), move, aiData->partnerMove))) ADJUST_SCORE(-8); break; @@ -2254,12 +2244,14 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) ADJUST_SCORE(-10); decreased = TRUE; } + break; case PROTECT_WIDE_GUARD: if(!(GetBattlerMoveTargetType(battlerAtk, predictedMove) & (MOVE_TARGET_FOES_AND_ALLY | MOVE_TARGET_BOTH))) { ADJUST_SCORE(-10); decreased = TRUE; } + break; case PROTECT_CRAFTY_SHIELD: if (!hasPartner) { @@ -3000,7 +2992,7 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) SetTypeBeforeUsingMove(move, battlerAtk); moveType = GetBattleMoveType(move); - + bool32 hasPartner = HasPartner(battlerAtk); u32 friendlyFireThreshold = GetFriendlyFireKOThreshold(battlerAtk); u32 noOfHitsToKOPartner = GetNoOfHitsToKOBattler(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING); @@ -3095,7 +3087,7 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) // Both Pokemon use Trick Room on the final turn of Trick Room to anticipate both opponents Protecting to stall out. // This unsets Trick Room and resets it with a full timer. case EFFECT_TRICK_ROOM: - if (hasPartner && gFieldStatuses & STATUS_FIELD_TRICK_ROOM && gFieldTimers.trickRoomTimer == gBattleTurnCounter + if (hasPartner && gFieldStatuses & STATUS_FIELD_TRICK_ROOM && gFieldTimers.trickRoomTimer == gBattleTurnCounter && ShouldSetFieldStatus(battlerAtk, STATUS_FIELD_TRICK_ROOM) && HasMoveWithEffect(battlerAtkPartner, MOVE_TRICK_ROOM) && RandomPercentage(RNG_AI_REFRESH_TRICK_ROOM_ON_LAST_TURN, DOUBLE_TRICK_ROOM_ON_LAST_TURN_CHANCE)) @@ -5741,8 +5733,8 @@ static s32 AI_AttacksPartner(u32 battlerAtk, u32 battlerDef, u32 move, s32 score u32 hitsToKO = GetNoOfHitsToKOBattler(battlerAtk, battlerDef, gAiThinkingStruct->movesetIndex, AI_ATTACKING); - if (GetMoveTarget(move) == MOVE_TARGET_FOES_AND_ALLY && hitsToKO > 0 && - (GetNoOfHitsToKOBattler(battlerAtk, FOE(battlerAtk), gAiThinkingStruct->movesetIndex, AI_ATTACKING) > 0 || GetNoOfHitsToKOBattler(battlerAtk, FOE(battlerDef), gAiThinkingStruct->movesetIndex, AI_ATTACKING) > 0)) + if (GetMoveTarget(move) == MOVE_TARGET_FOES_AND_ALLY && hitsToKO > 0 && + (GetNoOfHitsToKOBattler(battlerAtk, FOE(battlerAtk), gAiThinkingStruct->movesetIndex, AI_ATTACKING) > 0 || GetNoOfHitsToKOBattler(battlerAtk, FOE(battlerDef), gAiThinkingStruct->movesetIndex, AI_ATTACKING) > 0)) ADJUST_SCORE(BEST_EFFECT); if (hitsToKO > 0) diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 379ce723fd..6f470d3c84 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -111,11 +111,11 @@ u32 GetSwitchChance(enum ShouldSwitchScenario shouldSwitchScenario) static bool32 IsAceMon(u32 battler, u32 monPartyId) { - if (gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_ACE_POKEMON + if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_ACE_POKEMON && !gBattleStruct->battlerState[battler].forcedSwitch && monPartyId == CalculateEnemyPartyCountInSide(battler)-1) return TRUE; - if (gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_DOUBLE_ACE_POKEMON + if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_DOUBLE_ACE_POKEMON && !gBattleStruct->battlerState[battler].forcedSwitch && (monPartyId == CalculateEnemyPartyCount()-1 || monPartyId == CalculateEnemyPartyCount()-2)) return TRUE; @@ -215,7 +215,7 @@ static bool32 ShouldSwitchIfHasBadOdds(u32 battler) bool32 canBattlerWin1v1 = FALSE, isBattlerFirst, isBattlerFirstPriority; // Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer - if (!(gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) + if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; // Double Battles aren't included in AI_FLAG_SMART_MON_CHOICE. Defaults to regular switch in logic @@ -486,7 +486,7 @@ static bool32 FindMonThatAbsorbsOpponentsMove(u32 battler) bool32 isOpposingBattlerChargingOrInvulnerable = !BreaksThroughSemiInvulnerablity(opposingBattler, incomingMove) || IsTwoTurnNotSemiInvulnerableMove(opposingBattler, incomingMove); s32 i, j; - if (!(gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) + if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; if (GetMoveEffect(incomingMove) == EFFECT_HIDDEN_POWER && RandomPercentage(RNG_AI_SWITCH_ABSORBING_HIDDEN_POWER, SHOULD_SWITCH_ABSORBS_HIDDEN_POWER_PERCENTAGE)) return FALSE; @@ -618,7 +618,7 @@ static bool32 ShouldSwitchIfOpponentChargingOrInvulnerable(u32 battler) bool32 isOpposingBattlerChargingOrInvulnerable = !BreaksThroughSemiInvulnerablity(opposingBattler, incomingMove) || IsTwoTurnNotSemiInvulnerableMove(opposingBattler, incomingMove); - if (IsDoubleBattle() || !(gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) + if (IsDoubleBattle() || !(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; if (isOpposingBattlerChargingOrInvulnerable && gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE && RandomPercentage(RNG_AI_SWITCH_FREE_TURN, GetSwitchChance(SHOULD_SWITCH_FREE_TURN))) @@ -637,7 +637,7 @@ static bool32 ShouldSwitchIfTrapperInParty(u32 battler) s32 opposingBattler = GetOppositeBattler(battler); // Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer - if (!(gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) + if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; // Check if current mon has an ability that traps opponent @@ -681,7 +681,7 @@ static bool32 ShouldSwitchIfBadlyStatused(u32 battler) && RandomPercentage(RNG_AI_SWITCH_PERISH_SONG, GetSwitchChance(SHOULD_SWITCH_PERISH_SONG))) return SetSwitchinAndSwitch(battler, PARTY_SIZE); - if (gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING) + if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING) { //Yawn if (gStatuses3[battler] & STATUS3_YAWN @@ -869,7 +869,7 @@ static bool32 FindMonWithFlagsAndSuperEffective(u32 battler, u16 flags, u32 perc u16 move; // Similar functionality handled more thoroughly by ShouldSwitchIfHasBadOdds - if (gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING) + if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING) return FALSE; if (gLastLandedMoves[battler] == MOVE_NONE) @@ -1007,7 +1007,7 @@ static bool32 ShouldSwitchIfEncored(u32 battler) u32 opposingBattler = GetOppositeBattler(battler); // Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer - if (!(gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) + if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; // If not Encore'd don't switch @@ -1057,7 +1057,7 @@ static bool32 ShouldSwitchIfAttackingStatsLowered(u32 battler) s8 spAttackingStage = gBattleMons[battler].statStages[STAT_SPATK]; // Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer - if (!(gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) + if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; // Physical attacker @@ -1117,7 +1117,7 @@ bool32 ShouldSwitch(u32 battler) return FALSE; // Sequence Switching AI never switches mid-battle - if (gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SEQUENCE_SWITCHING) + if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SEQUENCE_SWITCHING) return FALSE; availableToSwitch = 0; @@ -1169,7 +1169,7 @@ bool32 ShouldSwitch(u32 battler) if (ShouldSwitchIfWonderGuard(battler)) return TRUE; - if ((gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING) && (CanMonSurviveHazardSwitchin(battler) == FALSE)) + if ((gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING) && (CanMonSurviveHazardSwitchin(battler) == FALSE)) return FALSE; if (ShouldSwitchIfTrapperInParty(battler)) return TRUE; @@ -1197,7 +1197,7 @@ bool32 ShouldSwitch(u32 battler) // Removing switch capabilites under specific conditions // These Functions prevent the "FindMonWithFlagsAndSuperEffective" from getting out of hand. // We don't use FindMonWithFlagsAndSuperEffective with AI_FLAG_SMART_SWITCHING, so we can bail early. - if (gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING) + if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING) return FALSE; if (CanUseSuperEffectiveMoveAgainstOpponents(battler)) return FALSE; @@ -1216,7 +1216,7 @@ bool32 ShouldSwitch(u32 battler) bool32 ShouldSwitchIfAllScoresBad(u32 battler) { u32 i, score, opposingBattler = GetOppositeBattler(battler); - if (!(gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) + if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) return FALSE; for (i = 0; i < MAX_MON_MOVES; i++) @@ -1268,7 +1268,7 @@ void ModifySwitchAfterMoveScoring(u32 battler) return; // Sequence Switching AI never switches mid-battle - if (gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SEQUENCE_SWITCHING) + if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SEQUENCE_SWITCHING) return; availableToSwitch = 0; @@ -2329,14 +2329,14 @@ u32 GetMostSuitableMonToSwitchInto(u32 battler, enum SwitchType switchType) GetAIPartyIndexes(battler, &firstId, &lastId); party = GetBattlerParty(battler); - if (gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SEQUENCE_SWITCHING) + if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SEQUENCE_SWITCHING) { bestMonId = GetNextMonInParty(party, firstId, lastId, battlerIn1, battlerIn2); return bestMonId; } // Only use better mon selection if AI_FLAG_SMART_MON_CHOICES is set for the trainer. - if (gAiThinkingStruct->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_MON_CHOICES && !IsDoubleBattle()) // Double Battles aren't included in AI_FLAG_SMART_MON_CHOICE. Defaults to regular switch in logic + if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_MON_CHOICES && !IsDoubleBattle()) // Double Battles aren't included in AI_FLAG_SMART_MON_CHOICE. Defaults to regular switch in logic { bestMonId = GetBestMonIntegrated(party, firstId, lastId, battler, opposingBattler, battlerIn1, battlerIn2, switchType); return bestMonId; diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 624dd7cf5a..e044f7b3e9 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -173,11 +173,17 @@ bool32 IsAiBattlerPredictingAbility(u32 battlerId) return BattlerHasAi(battlerId); } +bool32 CanAiPredictMove(void) +{ + return gAiThinkingStruct->aiFlags[B_POSITION_OPPONENT_LEFT] & AI_FLAG_PREDICT_MOVE + || gAiThinkingStruct->aiFlags[B_POSITION_OPPONENT_RIGHT] & AI_FLAG_PREDICT_MOVE; +} + bool32 IsBattlerPredictedToSwitch(u32 battler) { // Check for prediction flag on AI, whether they're using those predictions this turn, and whether the AI thinks the player should switch - if (gAiThinkingStruct->aiFlags[gAiLogicData->battlerDoingPrediction] & AI_FLAG_PREDICT_SWITCH - || gAiThinkingStruct->aiFlags[gAiLogicData->battlerDoingPrediction] & AI_FLAG_PREDICT_SWITCH) + if (gAiThinkingStruct->aiFlags[B_POSITION_OPPONENT_LEFT] & AI_FLAG_PREDICT_SWITCH + || gAiThinkingStruct->aiFlags[B_POSITION_OPPONENT_RIGHT] & AI_FLAG_PREDICT_SWITCH) { if (gAiLogicData->predictingSwitch && gAiLogicData->shouldSwitch & (1u << battler)) return TRUE; @@ -188,9 +194,8 @@ bool32 IsBattlerPredictedToSwitch(u32 battler) // Either a predicted move or the last used move from an opposing battler u32 GetIncomingMove(u32 battler, u32 opposingBattler, struct AiLogicData *aiData) { - if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_PREDICT_MOVE && aiData->predictingMove) + if (aiData->predictingMove && CanAiPredictMove()) return aiData->predictedMove[opposingBattler]; - return aiData->lastUsedMove[opposingBattler]; } @@ -5702,13 +5707,6 @@ s32 BattlerBenefitsFromAbilityScore(u32 battler, u32 ability, struct AiLogicData return WEAK_EFFECT; } -u32 GetThinkingBattler(u32 battler) -{ - if (gAiLogicData->aiPredictionInProgress) - return gAiLogicData->battlerDoingPrediction; - return battler; -} - bool32 IsNaturalEnemy(u32 speciesAttacker, u32 speciesTarget) { if (B_WILD_NATURAL_ENEMIES != TRUE) diff --git a/test/battle/ai/ai_check_viability.c b/test/battle/ai/ai_check_viability.c index 1407023ece..f18f7aceb1 100644 --- a/test/battle/ai/ai_check_viability.c +++ b/test/battle/ai/ai_check_viability.c @@ -339,3 +339,27 @@ AI_SINGLE_BATTLE_TEST("AI uses Skill Swap against Poison Heal") TURN { NOT_EXPECT_MOVE(opponent, MOVE_SKILL_SWAP); } } } + +AI_SINGLE_BATTLE_TEST("AI uses Quick Guard against Quick Attack when opponent would take poison damage") +{ + PASSES_RANDOMLY(PREDICT_MOVE_CHANCE, 100, RNG_AI_PREDICT_MOVE); + GIVEN { + PLAYER(SPECIES_RATTATA) { Moves(MOVE_QUICK_ATTACK); Status1(STATUS1_TOXIC_POISON); } + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT | AI_FLAG_PREDICT_MOVE); + OPPONENT(SPECIES_RATTATA) { Moves(MOVE_QUICK_GUARD, MOVE_TACKLE); } + } WHEN { + TURN { MOVE(player, MOVE_QUICK_ATTACK); EXPECT_MOVE(opponent, MOVE_QUICK_GUARD); } + } +} + +AI_SINGLE_BATTLE_TEST("AI uses Wide Guard against Earthquake when opponent would take poison damage") +{ + PASSES_RANDOMLY(PREDICT_MOVE_CHANCE, 100, RNG_AI_PREDICT_MOVE); + GIVEN { + PLAYER(SPECIES_RATTATA) { Moves(MOVE_EARTHQUAKE); Status1(STATUS1_TOXIC_POISON); } + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT | AI_FLAG_PREDICT_MOVE); + OPPONENT(SPECIES_RATTATA) { Moves(MOVE_WIDE_GUARD, MOVE_TACKLE); } + } WHEN { + TURN { MOVE(player, MOVE_EARTHQUAKE); EXPECT_MOVE(opponent, MOVE_WIDE_GUARD); } + } +}