From cbb6c4f14eeb1d554df256b312faf2a7c6ef50c8 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:02:12 +0100 Subject: [PATCH 01/19] Revert reversion (#8112) --- docs/book.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/book.toml b/docs/book.toml index 3282d8c8e7..a1e994eff3 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -1,6 +1,5 @@ [book] language = "en" -multilingual = false src = "." title = "pokeemerald-expansion" From 499986a8049d17d75839df164fbf4eae657fe87c Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:02:36 +0100 Subject: [PATCH 02/19] Clean up redundant todo (#8094) --- src/battle_util.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/battle_util.c b/src/battle_util.c index 9adfcc5016..c3cd8c5b54 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -5528,7 +5528,6 @@ bool32 CanBePoisoned(u32 battlerAtk, u32 battlerDef, u32 abilityAtk, u32 ability return FALSE; } -// TODO: check order of battlerAtk and battlerDef bool32 CanBeBurned(u32 battlerAtk, u32 battlerDef, u32 abilityDef) { if (CanSetNonVolatileStatus( @@ -10920,7 +10919,6 @@ bool32 PickupHasValidTarget(u32 battler) return FALSE; } -// TODO: Pass down weather as an arg bool32 IsBattlerWeatherAffected(u32 battler, u32 weatherFlags) { if (gBattleWeather & weatherFlags && HasWeatherEffect()) From c981fb3b2e0e39634525d4a4bd29ab6992d68568 Mon Sep 17 00:00:00 2001 From: grintoul1 Date: Mon, 3 Nov 2025 18:34:54 +0000 Subject: [PATCH 03/19] Fixes difficulty not being restored after tests --- test/battle/trainer_control.c | 3 +++ test/test_runner_battle.c | 1 + 2 files changed, 4 insertions(+) diff --git a/test/battle/trainer_control.c b/test/battle/trainer_control.c index 663a720d3d..1821cebcb5 100644 --- a/test/battle/trainer_control.c +++ b/test/battle/trainer_control.c @@ -188,6 +188,7 @@ TEST("Difficulty default to Normal is the trainer doesn't have a member for the CreateNPCTrainerPartyFromTrainer(testParty, &sTestTrainers[GetTrainerDifficultyLevelTest(currTrainer)][currTrainer], TRUE, BATTLE_TYPE_TRAINER); EXPECT(GetMonData(&testParty[0], MON_DATA_SPECIES) == SPECIES_MEWTWO); Free(testParty); + SetCurrentDifficultyLevel(DIFFICULTY_NORMAL); } TEST("Difficulty changes which party if used for NPCs if defined for the difficulty (EASY)") @@ -199,6 +200,7 @@ TEST("Difficulty changes which party if used for NPCs if defined for the difficu EXPECT(GetMonData(&testParty[0], MON_DATA_SPECIES) == SPECIES_METAPOD); EXPECT(GetMonData(&testParty[0], MON_DATA_LEVEL) == 1); Free(testParty); + SetCurrentDifficultyLevel(DIFFICULTY_NORMAL); } TEST("Difficulty changes which party if used for NPCs if defined for the difficulty (HARD)") @@ -210,6 +212,7 @@ TEST("Difficulty changes which party if used for NPCs if defined for the difficu EXPECT(GetMonData(&testParty[0], MON_DATA_SPECIES) == SPECIES_ARCEUS); EXPECT(GetMonData(&testParty[0], MON_DATA_LEVEL) == 99); Free(testParty); + SetCurrentDifficultyLevel(DIFFICULTY_NORMAL); } TEST("Difficulty changes which party if used for NPCs if defined for the difficulty (NORMAL)") diff --git a/test/test_runner_battle.c b/test/test_runner_battle.c index 157cec9018..395381d1d2 100644 --- a/test/test_runner_battle.c +++ b/test/test_runner_battle.c @@ -1344,6 +1344,7 @@ static void TearDownBattle(void) // Zero out the parties, data in them could potentially carry over ZeroPlayerPartyMons(); ZeroEnemyPartyMons(); + SetCurrentDifficultyLevel(DIFFICULTY_NORMAL); FreeMonSpritesGfx(); FreeBattleSpritesData(); From 6bcf9823c5d49971dfbf9cfb5a9d3360c2d2bc12 Mon Sep 17 00:00:00 2001 From: grintoul <166724814+grintoul1@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:58:03 +0000 Subject: [PATCH 04/19] Lock mdbook to v0.4.35 to fix docs not building (#8130) --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 874ebdd590..b2792e87de 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,7 +17,7 @@ jobs: fetch-depth: 0 - name: Install latest mdbook run: | - tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name') + tag="v0.4.35" url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz" mkdir mdbook curl -sSL $url | tar -xz --directory=./mdbook From 3c72ca11586c3ca96ca06bdb6cb2acb4ecec786a Mon Sep 17 00:00:00 2001 From: ghostyboyy97 <106448956+ghostyboyy97@users.noreply.github.com> Date: Tue, 4 Nov 2025 03:35:43 -0500 Subject: [PATCH 05/19] fix (AI scoring): shield dust considerations, IsMoveEffectInMinus self effect edge case, hitsToKO zero-case consideration (#8126) Co-authored-by: Alex <93446519+AlexOn1ine@users.noreply.github.com> --- include/battle_ai_util.h | 1 + src/battle_ai_main.c | 5 ++++- src/battle_ai_util.c | 22 ++++++++++++++++++---- test/battle/ability/shield_dust.c | 28 ++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 1b23dd7e1a..1964e45d53 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -155,6 +155,7 @@ bool32 IsAffectedByPowder(u32 battler, u32 ability, enum ItemHoldEffect holdEffe bool32 MovesWithCategoryUnusable(u32 attacker, u32 target, enum DamageCategory category); enum MoveComparisonResult AI_WhichMoveBetter(u32 move1, u32 move2, u32 battlerAtk, u32 battlerDef, s32 noOfHitsToKo); struct SimulatedDamage AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 battlerDef, uq4_12_t *typeEffectiveness, enum AIConsiderGimmick considerGimmickAtk, enum AIConsiderGimmick considerGimmickDef); +bool32 IsAdditionalEffectBlocked(u32 battlerAtk, u32 abilityAtk, u32 battlerDef, u32 abilityDef); struct SimulatedDamage AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, uq4_12_t *typeEffectiveness, enum AIConsiderGimmick considerGimmickAtk, enum AIConsiderGimmick considerGimmickDef, u32 weather); bool32 AI_IsDamagedByRecoil(u32 battler); u32 GetNoOfHitsToKO(u32 dmg, s32 hp); diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 2cc7cfe62c..0c8c0bb1cd 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -5069,7 +5069,7 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move) if (gBattleMons[battlerDef].speed > gBattleMons[battlerAtk].speed) ADJUST_SCORE(DECENT_EFFECT); break; -case EFFECT_GUARD_SPLIT: + case EFFECT_GUARD_SPLIT: { u32 atkDefense = gBattleMons[battlerAtk].defense; u32 defDefense = gBattleMons[battlerDef].defense; @@ -5590,6 +5590,9 @@ case EFFECT_GUARD_SPLIT: } else // consider move effects that hinder the target { + if (IsAdditionalEffectBlocked(battlerAtk, aiData->abilities[battlerAtk], battlerDef, aiData->abilities[battlerDef])) + continue; + switch (additionalEffect->moveEffect) { case MOVE_EFFECT_FLINCH: diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 4a397719c0..d866e5b0d0 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -701,6 +701,17 @@ bool32 IsDamageMoveUnusable(struct DamageContext *ctx) return FALSE; } +bool32 IsAdditionalEffectBlocked(u32 battlerAtk, u32 abilityAtk, u32 battlerDef, u32 abilityDef) +{ + if (gAiLogicData->holdEffects[battlerDef] == HOLD_EFFECT_COVERT_CLOAK) + return TRUE; + + if (abilityDef == ABILITY_SHIELD_DUST && !IsMoldBreakerTypeAbility(battlerAtk, abilityAtk)) + return TRUE; + + return FALSE; +} + static inline s32 GetDamageByRollType(s32 dmg, enum DamageRollType rollType) { if (rollType == DMG_ROLL_LOWEST) @@ -1073,6 +1084,9 @@ static bool32 AI_IsMoveEffectInPlus(u32 battlerAtk, u32 battlerDef, u32 move, s3 } else // consider move effects that hinder the target { + if (IsAdditionalEffectBlocked(battlerAtk, abilityAtk, battlerDef, abilityDef)) + continue; + switch (additionalEffect->moveEffect) { case MOVE_EFFECT_POISON: @@ -1107,7 +1121,7 @@ static bool32 AI_IsMoveEffectInPlus(u32 battlerAtk, u32 battlerDef, u32 move, s3 case MOVE_EFFECT_SP_DEF_MINUS_1: case MOVE_EFFECT_ACC_MINUS_1: case MOVE_EFFECT_EVS_MINUS_1: - if (CanLowerStat(battlerAtk, battlerDef, gAiLogicData, STAT_ATK + (additionalEffect->moveEffect - MOVE_EFFECT_ATK_MINUS_1)) && noOfHitsToKo != 1) + if (CanLowerStat(battlerAtk, battlerDef, gAiLogicData, STAT_ATK + (additionalEffect->moveEffect - MOVE_EFFECT_ATK_MINUS_1)) && noOfHitsToKo > 1) return TRUE; break; case MOVE_EFFECT_ATK_MINUS_2: @@ -1117,7 +1131,7 @@ static bool32 AI_IsMoveEffectInPlus(u32 battlerAtk, u32 battlerDef, u32 move, s3 case MOVE_EFFECT_SP_DEF_MINUS_2: case MOVE_EFFECT_ACC_MINUS_2: case MOVE_EFFECT_EVS_MINUS_2: - if (CanLowerStat(battlerAtk, battlerDef, gAiLogicData, STAT_ATK + (additionalEffect->moveEffect - MOVE_EFFECT_ATK_MINUS_2)) && noOfHitsToKo != 1) + if (CanLowerStat(battlerAtk, battlerDef, gAiLogicData, STAT_ATK + (additionalEffect->moveEffect - MOVE_EFFECT_ATK_MINUS_2)) && noOfHitsToKo > 1) return TRUE; break; default: @@ -1174,7 +1188,7 @@ static bool32 AI_IsMoveEffectInMinus(u32 battlerAtk, u32 battlerDef, u32 move, s case MOVE_EFFECT_ATK_DEF_DOWN: case MOVE_EFFECT_DEF_SPDEF_DOWN: if ((additionalEffect->self && abilityAtk != ABILITY_CONTRARY) - || (noOfHitsToKo != 1 && abilityDef == ABILITY_CONTRARY && !DoesBattlerIgnoreAbilityChecks(battlerAtk, abilityAtk, move))) + || (noOfHitsToKo > 1 && !additionalEffect->self && abilityDef == ABILITY_CONTRARY && !DoesBattlerIgnoreAbilityChecks(battlerAtk, abilityAtk, move))) return TRUE; break; case MOVE_EFFECT_RECHARGE: @@ -1195,7 +1209,7 @@ static bool32 AI_IsMoveEffectInMinus(u32 battlerAtk, u32 battlerDef, u32 move, s case MOVE_EFFECT_ACC_PLUS_2: case MOVE_EFFECT_ALL_STATS_UP: if ((additionalEffect->self && abilityAtk == ABILITY_CONTRARY) - || (noOfHitsToKo != 1 && !(abilityDef == ABILITY_CONTRARY && !DoesBattlerIgnoreAbilityChecks(battlerAtk, abilityAtk, move)))) + || (noOfHitsToKo > 1 && !additionalEffect->self && !(abilityDef == ABILITY_CONTRARY && !DoesBattlerIgnoreAbilityChecks(battlerAtk, abilityAtk, move)))) return TRUE; break; default: diff --git a/test/battle/ability/shield_dust.c b/test/battle/ability/shield_dust.c index 2d7358335d..2fc4f51d5c 100644 --- a/test/battle/ability/shield_dust.c +++ b/test/battle/ability/shield_dust.c @@ -188,3 +188,31 @@ SINGLE_BATTLE_TEST("Shield Dust does not prevent ability stat changes") MESSAGE("Vivillon's Speed fell!"); } } + +AI_SINGLE_BATTLE_TEST("AI will score secondary effects against shield dust correctly") +{ + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_OMNISCIENT); + GIVEN { + PLAYER(SPECIES_DUSTOX){ Ability(ABILITY_SHIELD_DUST); Moves(MOVE_GUST); } + OPPONENT(SPECIES_SUNFLORA){ Ability(ABILITY_EARLY_BIRD); Moves(MOVE_MYSTICAL_FIRE, MOVE_FIERY_DANCE); } + } WHEN { + TURN { + MOVE(player, MOVE_GUST); + EXPECT_MOVE(opponent, MOVE_FIERY_DANCE); + } + } +} + +AI_SINGLE_BATTLE_TEST("AI will score secondary effects against shield dust correctly when it has Mold Breaker") +{ + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_OMNISCIENT); + GIVEN { + PLAYER(SPECIES_DUSTOX){ Ability(ABILITY_SHIELD_DUST); Moves(MOVE_GUST); } + OPPONENT(SPECIES_SUNFLORA){ Ability(ABILITY_MOLD_BREAKER); Moves(MOVE_MYSTICAL_FIRE, MOVE_FIERY_DANCE); } + } WHEN { + TURN { + MOVE(player, MOVE_GUST); + EXPECT_MOVE(opponent, MOVE_MYSTICAL_FIRE); + } + } +} From b3965098f7fe62831e433dd25c4a9d60906f222d Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Tue, 4 Nov 2025 19:32:54 +0100 Subject: [PATCH 06/19] Fix berry blender not computing flavor correctly (#8113) --- src/berry_blender.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/berry_blender.c b/src/berry_blender.c index f953d7d5e8..adb28dab07 100644 --- a/src/berry_blender.c +++ b/src/berry_blender.c @@ -2432,6 +2432,7 @@ static void CalculatePokeblock(struct BlenderBerry *berries, struct Pokeblock *p } // Factor in max RPM and round + multiuseVar = maxRPM / 333 + 100; for (i = 0; i < FLAVOR_COUNT; i++) { s32 remainder; From be4ce5901fa7b53a1a169715f20eb56257950181 Mon Sep 17 00:00:00 2001 From: Bassoonian Date: Wed, 5 Nov 2025 14:48:53 +0100 Subject: [PATCH 07/19] Fix Kyurem typo in swap move tables (#8139) --- include/pokemon.h | 4 ++-- src/data/pokemon/form_change_table_pointers.h | 4 ++-- src/party_menu.c | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/include/pokemon.h b/include/pokemon.h index 296713e59d..8852ba3c97 100644 --- a/include/pokemon.h +++ b/include/pokemon.h @@ -654,10 +654,10 @@ extern const struct Fusion *const gFusionTablePointers[NUM_SPECIES]; #if P_FUSION_FORMS #if P_FAMILY_KYUREM #if P_FAMILY_RESHIRAM -extern const u16 gKyurenWhiteSwapMoveTable[][2]; +extern const u16 gKyuremWhiteSwapMoveTable[][2]; #endif //P_FAMILY_RESHIRAM #if P_FAMILY_ZEKROM -extern const u16 gKyurenBlackSwapMoveTable[][2]; +extern const u16 gKyuremBlackSwapMoveTable[][2]; #endif //P_FAMILY_ZEKROM #endif //P_FAMILY_KYUREM #endif //P_FUSION_FORMS diff --git a/src/data/pokemon/form_change_table_pointers.h b/src/data/pokemon/form_change_table_pointers.h index ad2e6a040d..1af386a897 100644 --- a/src/data/pokemon/form_change_table_pointers.h +++ b/src/data/pokemon/form_change_table_pointers.h @@ -38,14 +38,14 @@ const struct Fusion *const gFusionTablePointers[NUM_SPECIES] = #if P_FUSION_FORMS #if P_FAMILY_KYUREM #if P_FAMILY_RESHIRAM -const u16 gKyurenWhiteSwapMoveTable[][2] = +const u16 gKyuremWhiteSwapMoveTable[][2] = { {MOVE_SCARY_FACE, MOVE_FUSION_FLARE}, {MOVE_GLACIATE, MOVE_ICE_BURN}, }; #endif //P_FAMILY_RESHIRAM #if P_FAMILY_ZEKROM -const u16 gKyurenBlackSwapMoveTable[][2] = +const u16 gKyuremBlackSwapMoveTable[][2] = { {MOVE_SCARY_FACE, MOVE_FUSION_BOLT}, {MOVE_GLACIATE, MOVE_FREEZE_SHOCK}, diff --git a/src/party_menu.c b/src/party_menu.c index 891066dd41..04b2233102 100644 --- a/src/party_menu.c +++ b/src/party_menu.c @@ -6431,11 +6431,11 @@ static void Task_TryItemUseFusionChange(u8 taskId) #if P_FAMILY_KYUREM #if P_FAMILY_RESHIRAM if (gTasks[taskId].tExtraMoveHandling == SWAP_EXTRA_MOVES_KYUREM_WHITE) - SwapFusionMonMoves(mon, gKyurenWhiteSwapMoveTable, FUSE_MON); + SwapFusionMonMoves(mon, gKyuremWhiteSwapMoveTable, FUSE_MON); #endif //P_FAMILY_RESHIRAM #if P_FAMILY_ZEKROM if (gTasks[taskId].tExtraMoveHandling == SWAP_EXTRA_MOVES_KYUREM_BLACK) - SwapFusionMonMoves(mon, gKyurenBlackSwapMoveTable, FUSE_MON); + SwapFusionMonMoves(mon, gKyuremBlackSwapMoveTable, FUSE_MON); #endif //P_FAMILY_ZEKROM #endif //P_FAMILY_KYUREM if (gTasks[taskId].moveToLearn != 0) @@ -6446,11 +6446,11 @@ static void Task_TryItemUseFusionChange(u8 taskId) #if P_FAMILY_KYUREM #if P_FAMILY_RESHIRAM if (gTasks[taskId].tExtraMoveHandling == SWAP_EXTRA_MOVES_KYUREM_WHITE) - SwapFusionMonMoves(mon, gKyurenWhiteSwapMoveTable, UNFUSE_MON); + SwapFusionMonMoves(mon, gKyuremWhiteSwapMoveTable, UNFUSE_MON); #endif //P_FAMILY_RESHIRAM #if P_FAMILY_ZEKROM if (gTasks[taskId].tExtraMoveHandling == SWAP_EXTRA_MOVES_KYUREM_BLACK) - SwapFusionMonMoves(mon, gKyurenBlackSwapMoveTable, UNFUSE_MON); + SwapFusionMonMoves(mon, gKyuremBlackSwapMoveTable, UNFUSE_MON); #endif //P_FAMILY_ZEKROM #endif //P_FAMILY_KYUREM if ( gTasks[taskId].tExtraMoveHandling == FORGET_EXTRA_MOVES) From cfcce251d0b557add8fc1fab76d9386b54b82644 Mon Sep 17 00:00:00 2001 From: PhallenTree <168426989+PhallenTree@users.noreply.github.com> Date: Wed, 5 Nov 2025 13:49:12 +0000 Subject: [PATCH 08/19] Fix typo in Electrode-Hisui pokedex entry (#8143) --- src/data/pokemon/species_info/gen_1_families.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/pokemon/species_info/gen_1_families.h b/src/data/pokemon/species_info/gen_1_families.h index c6f68920e0..daae4cc252 100644 --- a/src/data/pokemon/species_info/gen_1_families.h +++ b/src/data/pokemon/species_info/gen_1_families.h @@ -12841,7 +12841,7 @@ const struct SpeciesInfo gSpeciesInfoGen1[] = .height = 5, .weight = 130, .description = COMPOUND_STRING( - "It esembles an ancient Poké Ball design.\n" + "It resembles an ancient Poké Ball design.\n" "When excited, it discharges the electric\n" "current it has stored in its belly, then\n" "lets out a great, uproarious laugh."), From d1428d7966a1695d007c122976aa0096b464cef5 Mon Sep 17 00:00:00 2001 From: Raymond Dodge Date: Wed, 5 Nov 2025 11:05:01 -0500 Subject: [PATCH 09/19] Update mdbook to v0.5.0-beta.1 (#8133) --- .github/workflows/docs.yml | 2 +- docs/fix_links.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b2792e87de..e02d931e28 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,7 +17,7 @@ jobs: fetch-depth: 0 - name: Install latest mdbook run: | - tag="v0.4.35" + tag="v0.5.0-beta.1" url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz" mkdir mdbook curl -sSL $url | tar -xz --directory=./mdbook diff --git a/docs/fix_links.py b/docs/fix_links.py index 6e2eaec485..3a4184b72c 100644 --- a/docs/fix_links.py +++ b/docs/fix_links.py @@ -46,6 +46,6 @@ if __name__ == '__main__': sys.exit(0) context, book = json.load(sys.stdin) - proc_items(book['sections']) + proc_items(book['items']) print(json.dumps(book)) From c8159ba18210d68e92483cd5202f88f491abc5f0 Mon Sep 17 00:00:00 2001 From: hedara90 <90hedara@gmail.com> Date: Wed, 5 Nov 2025 21:45:39 +0100 Subject: [PATCH 10/19] Reset saveblock data between test runs (#8145) Co-authored-by: Hedara --- src/battle_main.c | 5 ++++- test/test_runner.c | 15 ++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/battle_main.c b/src/battle_main.c index ad5ccac4ba..cd76bc827b 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -3002,7 +3002,10 @@ static void ClearSetBScriptingStruct(void) memset(&gBattleScripting, 0, sizeof(gBattleScripting)); gBattleScripting.windowsType = temp; - gBattleScripting.battleStyle = gSaveBlock2Ptr->optionsBattleStyle; + if (TESTING) + gBattleScripting.battleStyle = OPTIONS_BATTLE_STYLE_SET; + else + gBattleScripting.battleStyle = gSaveBlock2Ptr->optionsBattleStyle; gBattleScripting.expOnCatch = (B_EXP_CATCH >= GEN_6); gBattleScripting.specialTrainerBattleType = specialBattleType; } diff --git a/test/test_runner.c b/test/test_runner.c index 450283ab58..ac4b20cd2c 100644 --- a/test/test_runner.c +++ b/test/test_runner.c @@ -1,4 +1,5 @@ #include +#include "fake_rtc.h" #include "global.h" #include "gpu_regs.h" #include "load_save.h" @@ -184,6 +185,13 @@ void TestRunner_CheckMemory(void) } } +static void ClearSaveBlocks(void) +{ + ClearSav1(); + ClearSav2(); + ClearSav3(); +} + void CB2_TestRunner(void) { top: @@ -201,17 +209,13 @@ top: gTestRunnerState.filterMode = DetectFilterMode(gTestRunnerArgv); MoveSaveBlocks_ResetHeap(); - ClearSav1(); - ClearSav2(); - ClearSav3(); gIntrTable[7] = Intr_Timer2; - gSaveBlock2Ptr->optionsBattleStyle = OPTIONS_BATTLE_STYLE_SET; - // The current test restarted the ROM (e.g. by jumping to NULL). if (gPersistentTestRunnerState.address != 0) { + ClearSaveBlocks(); gTestRunnerState.test = __start_tests; while ((uintptr_t)gTestRunnerState.test != gPersistentTestRunnerState.address) { @@ -255,6 +259,7 @@ top: break; case STATE_ASSIGN_TEST: + ClearSaveBlocks(); while (1) { if (gTestRunnerState.test == __stop_tests) From 1f8a40962ad9af13e4034139e4550e35affb1830 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Thu, 6 Nov 2025 01:21:13 +0100 Subject: [PATCH 11/19] Allow to send active mon to PC when capturing a Pokemon (#8111) --- src/party_menu.c | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/party_menu.c b/src/party_menu.c index 04b2233102..3964716afa 100644 --- a/src/party_menu.c +++ b/src/party_menu.c @@ -1512,15 +1512,7 @@ static void HandleChooseMonSelection(u8 taskId, s8 *slotPtr) case PARTY_ACTION_SEND_MON_TO_BOX: { u8 partyId = (u8)*slotPtr; - if (partyId == 0 || ((gBattleTypeFlags & BATTLE_TYPE_DOUBLE) && partyId == 1)) - { - // Can't select if mon is currently on the field - PlaySE(SE_FAILURE); - DisplayPartyMenuMessage(gText_CannotSendMonToBoxActive, FALSE); - ScheduleBgCopyTilemapToVram(2); - gTasks[taskId].func = Task_ReturnToChooseMonAfterText; - } - else if ((gBattleTypeFlags & BATTLE_TYPE_MULTI) && partyId >= (PARTY_SIZE / 2)) + if ((gBattleTypeFlags & BATTLE_TYPE_MULTI) && partyId >= (PARTY_SIZE / 2)) { // Can't select if mon doesn't belong to you PlaySE(SE_FAILURE); From be7646b67a0d88f2bfe21c390b6022ae5f9e66c5 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Thu, 6 Nov 2025 19:16:19 +0100 Subject: [PATCH 12/19] Allow vs seekers to work with script not starting with trainerbattle (#8062) Co-authored-by: pkmnsnfrn --- asm/macros/event.inc | 6 ++ docs/SUMMARY.md | 1 + docs/tutorials/vs_seeker.md | 115 ++++++++++++++++++++++++++++++++++++ include/battle_setup.h | 2 + include/config/item.h | 2 +- include/config/overworld.h | 3 + include/vs_seeker.h | 1 + src/battle_main.c | 1 + src/battle_setup.c | 37 +++++++++--- src/vs_seeker.c | 46 +++++++++++++-- 10 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 docs/tutorials/vs_seeker.md diff --git a/asm/macros/event.inc b/asm/macros/event.inc index 56b5d1d9a0..bc55661bd6 100644 --- a/asm/macros/event.inc +++ b/asm/macros/event.inc @@ -2670,3 +2670,9 @@ callnative ScriptChangeFollowerNPCBattlePartner .2byte \battlePartner .endm + +@ VS Seeker + .macro vsseeker_rematchid rematchId:req + callnative NativeVsSeekerRematchId, requests_effects=1 + .2byte \rematchId + .endm diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 8d1e4e9cdb..a38a539bef 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -37,6 +37,7 @@ - [How to use Follower NPCs](tutorials/how_to_follower_npc.md) - [Time-Based Encounters](tutorials/how_to_time_of_day_encounters.md) - [How to use Trainer Party Pools](tutorials/how_to_trainer_party_pool.md) + - [Vs. Seeker](tutorials/vs_seeker.md) - [Changelog](./CHANGELOG.md) - [1.13.x]() - [Version 1.13.3](changelogs/1.13.x/1.13.3.md) diff --git a/docs/tutorials/vs_seeker.md b/docs/tutorials/vs_seeker.md new file mode 100644 index 0000000000..136465a5b4 --- /dev/null +++ b/docs/tutorials/vs_seeker.md @@ -0,0 +1,115 @@ +# `pokemerald-expansion` Vs. Seeker + +## What is the Vs. Seeker? +The Vs. Seeker is a Key Item that is used to battle Trainers that the player has battled previously. + +When used, the Vs. Seeker sends out a signal that allows the player to find other Trainers who want a rematch. This signal affects all Trainers that are on-screen. Once used on Trainers that can be rematched, the device cannot be used again until it is charged. The player does this by walking a specific number of steps. The effect on the Trainers wears off if they are battled, the player leaves the area, or the player walks a specific number of steps. If the player attempts to use the Vs. Seeker when it is not fully charged, the player will be told how many steps remain until it is. After the player uses the Vs. Seeker, some Trainers may have their team changed from their first battle. + +## How is the Vs. Seeker enabled? +### Users +Vs. Seeker functionality is enabled by setting `I_VS_SEEKER_CHARGING` to `TRUE`. + +### Players +`ITEM_VS_SEEKER` can only be used outside of battle. It can be used from the bag or registered to be used from the field. + +Usage of the Vs. Seeker will ALWAYS fail unless all of the conditions are met: +* Player has at least five badges +* There is an NPC on screen that has previously been defeated +* Player is not inside of a building +* The Vs. Seeker is fully recharged + +#### Charge +If the player has `ITEM_VS_SEEKER` and at least five badges, the Vs. Seeker will be charged by walking steps. The Vs. Seeker is fully charged once the player has walked `VSSEEKER_RECHARGE_STEPS`, which is `100` by default. The Vs. Seeker's charge is depleted if the player uses the item. + +### How does Match Call interact with the Vs. Seeker? +When `I_VS_SEEKER_CHARGING` is enabled, the Match Call does not function at all. Trainers will never be rematch eligible outside of the use of the Vs. Seeker. + +## How does the Vs. Seeker choose a Trainer? + +When the Vs. Seeker is successfully used, every Trainer on screen is individually queried. There is a 31% chance that the Trainer will want a rematch. +Objects listed in `regularTrainersOnLand` or `regularTrainersInWater` are considered Land/Water objects. + +| Status | Is Land/Water Object | Emote | New Movement Type | +| --- | --- | --- | --- | +| Wants Rematch | Yes | `MOVEMENT_ACTION_EMOTE_DOUBLE_EXCL_MARK` | `MOVEMENT_TYPE_COUNTER_CLOCKWISE` | +| Wants Rematch | No | `MOVEMENT_ACTION_EMOTE_DOUBLE_EXCL_MARK` | `MOVEMENT_TYPE_FACE_DOWN` | +| Does Not Want Rematch | - | `MOVEMENT_ACTION_EMOTE_X` | none | +| Has Not Been Fought | - | `MOVEMENT_ACTION_EMOTE_EXCLAMATION_MARK` | none | + +### Rematch Table + +| Sequence | Trainer ID | +| ---------- | ---------------- | +| 1st Battle | `TRAINER_ROSE_1` | +| 2nd Battle | `TRAINER_ROSE_2` | +| 3rd Battle | `TRAINER_ROSE_3` | +| 4th Battle | `TRAINER_ROSE_4` | +| 5th Battle | `TRAINER_ROSE_5` | + +The game determines which version of the Trainer you'll fight next by following these rules: + +1. Start with the next Trainer in the sequence after the one that has been defeated. If there are no more, the battle is against the last listed Trainer. +2. If that next Trainer hasn't been unlocked yet, the battle is against the latest available unlocked version. +3. If the next Trainer is unlocked but not yet defeated, the battle is against that version. +4. If the next Trainer has already been defeated, check the next one in the sequence. + +## How do users implement rematches with the Vs. Seeker? +### Existing `pokemerald` Trainers +No extra work is required. With the exception of Wally, Gym Leaders and Elite Four, all of the rematchable Trainers in Emerald will work with the Vs. Seeker without any changes. +### New Trainers +#### Party / `gRematchTable` +Each of the rematches for the Trainer must be defined as seperate Trainers in `src/data/trainers.party` and `include/constants/opponents`. For example, `TRAINER_CALVIN_1` also has `TRAINER_CALVIN_2`,`TRAINER_CALVIN_3`,`TRAINER_CALVIN_4`, and `TRAINER_CALVIN_5`. + +Once all of those constants and parties are defined, a new row must be added to `gRematchTable` (located in in `src/battle_setup.c`). The row header should be a rematch ID, which can be added in `include/constants/rematches.h`. The row contents must be the five constants created for the new parties, with the lat argument being the constant of the map (`include/constants/map_groups.h`) where the Trainer is placed. + +If a Trainer is intended to have less than five unique rematch parties, the extra slots can be filled with the last available Trainer ID. + +```c +// This Trainer only has two teams. + [REMATCH_ROSE] = REMATCH(TRAINER_ROSE_1, TRAINER_ROSE_2, TRAINER_ROSE_2, TRAINER_ROSE_2, TRAINER_ROSE_2, MAP_ROUTE118), +``` + +WARNING: Rematch IDs should be placed BEFORE `REMATCH_WALLY_VR`. Trainers below that are treated as "special Trainers" that are not triggered by the Vs. Seeker. + +#### Scripts +The trainer's object needs to have a script that begins with a method to signify what this object's trainer ID is. + +#### `trainerbattle` +``` +Route103_EventScript_Daisy:: + trainerbattle_single TRAINER_DAISY, Route103_Text_DaisyIntro, Route103_Text_DaisyDefeated + msgbox Route103_Text_DaisyPostBattle, MSGBOX_AUTOCLOSE + end +``` + +Daisy is using one of the `trainerbattle` macros, which has the trainer battle macro in the first command of the script. Most trainers in `pokeemerald` use this pattern. + +##### `vsseeker_rematchid` +``` +Route102_EventScript_Calvin:: + vsseeker_rematchid TRAINER_CALVIN_1 + applymovement LOCALID_CALVIN, CalvinMovementTest + waitmovement 0 + trainerbattle_single TRAINER_CALVIN_1, Route102_Text_CalvinIntro, Route102_Text_CalvinDefeated, Route102_EventScript_CalvinRegisterMatchCallAfterBattle + specialvar VAR_RESULT, ShouldTryRematchBattle + goto_if_eq VAR_RESULT, TRUE, Route102_EventScript_CalvinRematch + setvar VAR_0x8004, TRAINER_CALVIN_1 + specialvar VAR_RESULT, IsTrainerRegistered + goto_if_eq VAR_RESULT, FALSE, Route102_EventScript_CalvinTryRegister + msgbox Route102_Text_CalvinPostBattle, MSGBOX_DEFAULT + release + end +``` + +If the trainer has other script commands before the eventual `trainerbattle` macro, the first command in the script needs to be `vsseeker_rematchid`. This macro does nothing but takes a single argument, which should be the same as the first Trainer ID for this trainer. + +#### `MOVEMENT_TYPE_COUNTER_CLOCKWISE` +If you want Trainers to spin once they are eligible for a rematch, their overworld graphics object ID (`include/constants/event_objects.h`) must be listed in either `regularTrainersOnLand` or `regularTrainersInWater`.Otherwise they will adopt the movement type `MOVEMENT_TYPE_FACE_DOWN`. + +## What can be customized about the Vs. Seeker? +* **Unlock Conditions**: The next "level" of rematches is unlocked when a specific flag is set. The flags that are currently used in `GetGameProgressFlags` can be changed to flags that better suit your game. +* **Recharge Steps**: `VSSEEKER_RECHARGE_STEPS` is initally set to 100, but this value can be changed to any number under 256. +* **Badge Requirement**: `HasAtLeastFiveBadges` is used to check if the Vs. Seeker will successfully work. You can customize the number of badges by changing `REMATCH_BADGE_COUNT` or otherwise alterting the function. + +## What are the limitations of the Vs. Seeker? +The Vs. Seeker does not currently work with Gym Leaders. There is a bug filed to hopefully fix this in the future. diff --git a/include/battle_setup.h b/include/battle_setup.h index aa01178d8d..1605345260 100644 --- a/include/battle_setup.h +++ b/include/battle_setup.h @@ -90,12 +90,14 @@ const u8 *GetTrainerALoseText(void); const u8 *GetTrainerBLoseText(void); const u8 *GetTrainerWonSpeech(void); void UpdateRematchIfDefeated(s32 rematchTableId); +void ClearCurrentTrainerWantRematchVsSeeker(void); void IncrementRematchStepCounter(void); void TryUpdateRandomTrainerRematches(u16 mapGroup, u16 mapNum); bool32 DoesSomeoneWantRematchIn(u16 mapGroup, u16 mapNum); bool32 IsRematchTrainerIn(u16 mapGroup, u16 mapNum); u16 GetLastBeatenRematchTrainerId(u16 trainerId); bool8 ShouldTryRematchBattle(void); +bool8 ShouldTryRematchBattleForTrainerId(u16 trainerId); bool8 IsTrainerReadyForRematch(void); void ShouldTryGetTrainerScript(void); u16 CountBattledRematchTeams(u16 trainerId); diff --git a/include/config/item.h b/include/config/item.h index 96156b0db8..4222754ead 100644 --- a/include/config/item.h +++ b/include/config/item.h @@ -36,7 +36,7 @@ #define I_REPEL_LURE_MENU TRUE // If TRUE, the player is able to choose which Repel/Lure to use once the previous one runs out. Cursor position is saved by VAR_LAST_REPEL_LURE_USED if not 0. // Vs. Seeker -#define I_VS_SEEKER_CHARGING 0 // If this flag is assigned, the Vs Seeker functionality will be enabled. When the player has the Vs. Seeker, Match Call rematch functions will stop working. +#define I_VS_SEEKER_CHARGING 0 // If this flag is assigned, the Vs Seeker functionality will be enabled. When the player has the Vs. Seeker, Match Call rematch functions will stop working. Documentation for the Vs. Seeker can be found in docs/tutorials/vs_seeker.md. // Fishing #define I_FISHING_BITE_ODDS GEN_LATEST // In Gen 1 and Gen 2, the Old Rod has a 100% chance for a bite, Good Rod has a 66% chance for a bite, and Super Rod has a 50% chance for a bite. In Gen 3, all rods have a base 50% chance for a bite. In Gen 4 onwards, the Old Rod has a base 25% chance for a bite, Good Rod has a 50% chance for a bite, and Super Rod has a 75% chance for a bite. diff --git a/include/config/overworld.h b/include/config/overworld.h index 480c5dbcea..cb110b4226 100644 --- a/include/config/overworld.h +++ b/include/config/overworld.h @@ -142,4 +142,7 @@ // Berry Blender #define BERRY_BLENDER_THROW_ALL_BERRIES_AT_ONCE TRUE // This is a small little addition, that basically speeds up the animation where all players' berries are thrown into the blender. Self-explanatory I hope! +// Trainer Rematches +#define OW_REMATCH_BADGE_COUNT 5 // Number of badges necessary before the match call or vs seeker features allow rematches + #endif // GUARD_CONFIG_OVERWORLD_H diff --git a/include/vs_seeker.h b/include/vs_seeker.h index d6795432b0..749aaeab57 100644 --- a/include/vs_seeker.h +++ b/include/vs_seeker.h @@ -9,6 +9,7 @@ void MapResetTrainerRematches(u16 mapGroup, u16 mapNum); void ClearRematchMovementByTrainerId(void); u16 GetRematchTrainerIdVSSeeker(u16 trainerId); bool32 IsVsSeekerEnabled(void); +void NativeVsSeekerRematchId(struct ScriptContext *ctx); #define VSSEEKER_RECHARGE_STEPS 100 diff --git a/src/battle_main.c b/src/battle_main.c index cd76bc827b..15dfe0a3e4 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -5668,6 +5668,7 @@ static void FreeResetData_ReturnToOvOrDoEvolutions(void) else gSaveBlock3Ptr->dexNavChain = 0; + ClearCurrentTrainerWantRematchVsSeeker(); gDexNavSpecies = SPECIES_NONE; ResetSpriteData(); if (!(gBattleTypeFlags & (BATTLE_TYPE_LINK diff --git a/src/battle_setup.c b/src/battle_setup.c index 6a6f529ea6..a6d833c417 100644 --- a/src/battle_setup.c +++ b/src/battle_setup.c @@ -1741,6 +1741,20 @@ static void ClearTrainerWantRematchState(const struct RematchTrainer *table, u16 #endif //FREE_MATCH_CALL } +void ClearCurrentTrainerWantRematchVsSeeker(void) +{ +#if FREE_MATCH_CALL == FALSE + if ((gBattleTypeFlags & BATTLE_TYPE_TRAINER) && FlagGet(I_VS_SEEKER_CHARGING) && (I_VS_SEEKER_CHARGING != 0)) + { + for (u32 i = 0; i < REMATCH_TABLE_ENTRIES; i++) + { + if (gSaveBlock1Ptr->trainerRematches[i] == TRAINER_BATTLE_PARAM.opponentA) + gSaveBlock1Ptr->trainerRematches[i] = 0; + } + } +#endif //FREE_MATCH_CALL +} + static u32 GetTrainerMatchCallFlag(u32 trainerId) { s32 i; @@ -1772,12 +1786,16 @@ static bool8 WasSecondRematchWon(const struct RematchTrainer *table, u16 firstBa return FALSE; if (!HasTrainerBeenFought(table[tableId].trainerIds[1])) return FALSE; - + if (I_VS_SEEKER_CHARGING) + { + if (gSaveBlock1Ptr->trainerRematches[tableId] == 0) + return FALSE; + } return TRUE; } #if FREE_MATCH_CALL == FALSE -static bool32 HasAtLeastFiveBadges(void) +static bool32 HasEnoughBadgesForRematch(void) { s32 i, count; @@ -1785,7 +1803,7 @@ static bool32 HasAtLeastFiveBadges(void) { if (FlagGet(gBadgeFlags[i]) == TRUE) { - if (++count >= 5) + if (++count >= OW_REMATCH_BADGE_COUNT) return TRUE; } } @@ -1799,7 +1817,7 @@ static bool32 HasAtLeastFiveBadges(void) void IncrementRematchStepCounter(void) { #if FREE_MATCH_CALL == FALSE - if (!HasAtLeastFiveBadges()) + if (!HasEnoughBadgesForRematch()) return; if (IsVsSeekerEnabled()) @@ -1815,7 +1833,7 @@ void IncrementRematchStepCounter(void) #if FREE_MATCH_CALL == FALSE static bool32 IsRematchStepCounterMaxed(void) { - if (HasAtLeastFiveBadges() && gSaveBlock1Ptr->trainerRematchStepCounter >= STEP_COUNTER_MAX) + if (HasEnoughBadgesForRematch() && gSaveBlock1Ptr->trainerRematchStepCounter >= STEP_COUNTER_MAX) return TRUE; else return FALSE; @@ -1855,10 +1873,15 @@ u16 GetLastBeatenRematchTrainerId(u16 trainerId) bool8 ShouldTryRematchBattle(void) { - if (IsFirstTrainerIdReadyForRematch(gRematchTable, TRAINER_BATTLE_PARAM.opponentA)) + return ShouldTryRematchBattleForTrainerId(TRAINER_BATTLE_PARAM.opponentA); +} + +bool8 ShouldTryRematchBattleForTrainerId(u16 trainerId) +{ + if (IsFirstTrainerIdReadyForRematch(gRematchTable, trainerId)) return TRUE; - return WasSecondRematchWon(gRematchTable, TRAINER_BATTLE_PARAM.opponentA); + return WasSecondRematchWon(gRematchTable, trainerId); } bool8 IsTrainerReadyForRematch(void) diff --git a/src/vs_seeker.c b/src/vs_seeker.c index 1a7dc68c72..8669ca3f47 100644 --- a/src/vs_seeker.c +++ b/src/vs_seeker.c @@ -31,6 +31,8 @@ #include "constants/trainer_types.h" #include "constants/field_effects.h" +// Documentation for the Vs. Seeker can be found in docs/tutorials/vs_seeker.md. + enum { VSSEEKER_NOT_CHARGED, @@ -230,6 +232,9 @@ bool8 UpdateVsSeekerStepCounter(void) if (!I_VS_SEEKER_CHARGING) return FALSE; + // This condition helps in case your save file is switching between vs seeker and matchcall + if (gSaveBlock1Ptr->trainerRematchStepCounter > VSSEEKER_RECHARGE_STEPS && gSaveBlock1Ptr->trainerRematchStepCounter <= 0xFF) + gSaveBlock1Ptr->trainerRematchStepCounter = 0; if (CheckBagHasItem(ITEM_VS_SEEKER, 1)) { if ((gSaveBlock1Ptr->trainerRematchStepCounter & 0xFF) < VSSEEKER_RECHARGE_STEPS) @@ -376,8 +381,11 @@ static void GatherNearbyTrainerInfo(void) if (templates[objectEventIdx].trainerType != TRAINER_TYPE_NORMAL && templates[objectEventIdx].trainerType != TRAINER_TYPE_BURIED) continue; + u16 trainerIdx = GetTrainerFlagFromScript(templates[objectEventIdx].script); + if (trainerIdx == TRAINER_NONE) + continue; sVsSeeker->trainerInfo[vsSeekerObjectIdx].script = templates[objectEventIdx].script; - sVsSeeker->trainerInfo[vsSeekerObjectIdx].trainerIdx = GetTrainerFlagFromScript(templates[objectEventIdx].script); + sVsSeeker->trainerInfo[vsSeekerObjectIdx].trainerIdx = trainerIdx; sVsSeeker->trainerInfo[vsSeekerObjectIdx].localId = templates[objectEventIdx].localId; TryGetObjectEventIdByLocalIdAndMap(templates[objectEventIdx].localId, gSaveBlock1Ptr->location.mapNum, gSaveBlock1Ptr->location.mapGroup, &objectEventId); sVsSeeker->trainerInfo[vsSeekerObjectIdx].objectEventId = objectEventId; @@ -707,9 +715,31 @@ static u16 GetTrainerFlagFromScript(const u8 *script) { // The trainer flag is located 3 bytes (command + flags + localIdA) from the script pointer, assuming the trainerbattle command is first in the script. // Because scripts are unaligned, and because the ARM processor requires shorts to be 16-bit aligned, this function needs to perform explicit bitwise operations to get the correct flag. - script += 3; - u16 trainerFlag = script[0]; - trainerFlag |= script[1] << 8; + u16 trainerFlag; + switch (script[0]) + { + case 0x5c: + script += 3; + trainerFlag = script[0]; + trainerFlag |= script[1] << 8; + break; + case 0x23: + u32 callnativeFunc = (((((script[4] << 8) + script[3]) << 8) + script[2]) << 8) + script[1]; + if (callnativeFunc == ((u32)NativeVsSeekerRematchId | 0xA000000)) // | 0xA000000 corresponds to the request_effects=1 version of the function + { + script += 5; + trainerFlag = script[0]; + trainerFlag |= script[1] << 8; + } + else + { + trainerFlag = TRAINER_NONE; + } + break; + default: + trainerFlag = TRAINER_NONE; + break; + } return trainerFlag; } @@ -788,6 +818,14 @@ static u8 GetCurVsSeekerResponse(s32 vsSeekerIdx, u16 trainerIdx) } #endif //FREE_MATCH_CALL +void NativeVsSeekerRematchId(struct ScriptContext *ctx) +{ + u16 trainerId = ScriptReadHalfword(ctx); + if (ctx->breakOnTrainerBattle && HasTrainerBeenFought(trainerId) && !ShouldTryRematchBattleForTrainerId(trainerId)) + StopScript(ctx); +} + + static void StartAllRespondantIdleMovements(void) { #if FREE_MATCH_CALL == FALSE From 97b115d5722f77ef791784d63b34e29b65011bcb Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Fri, 7 Nov 2025 06:10:42 +0100 Subject: [PATCH 13/19] Adds an auto-generated include file of script commands (#8156) --- Makefile | 8 ++++++++ include/battle_setup.h | 1 + src/battle_setup.c | 7 +++++++ src/trainer_see.c | 16 ++++++++++++++-- src/vs_seeker.c | 5 +++-- tools/misc/make_scr_cmd_constants.py | 25 +++++++++++++++++++++++++ 6 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 tools/misc/make_scr_cmd_constants.py diff --git a/Makefile b/Makefile index 8e5259ad1e..fa2420e105 100644 --- a/Makefile +++ b/Makefile @@ -195,10 +195,18 @@ ALL_LEARNABLES_JSON := $(LEARNSET_HELPERS_BUILD_DIR)/all_learnables.json WILD_ENCOUNTERS_TOOL_DIR := $(TOOLS_DIR)/wild_encounters AUTO_GEN_TARGETS += $(DATA_SRC_SUBDIR)/wild_encounters.h +MISC_TOOL_DIR := $(TOOLS_DIR)/misc +AUTO_GEN_TARGETS += $(INCLUDE_DIRS)/constants/script_commands.h + $(DATA_SRC_SUBDIR)/wild_encounters.h: $(DATA_SRC_SUBDIR)/wild_encounters.json $(WILD_ENCOUNTERS_TOOL_DIR)/wild_encounters_to_header.py $(INCLUDE_DIRS)/config/overworld.h $(INCLUDE_DIRS)/config/dexnav.h python3 $(WILD_ENCOUNTERS_TOOL_DIR)/wild_encounters_to_header.py > $@ +$(INCLUDE_DIRS)/constants/script_commands.h: $(MISC_TOOL_DIR)/make_scr_cmd_constants.py $(DATA_ASM_SUBDIR)/script_cmd_table.inc + python3 $(MISC_TOOL_DIR)/make_scr_cmd_constants.py + $(C_BUILDDIR)/wild_encounter.o: c_dep += $(DATA_SRC_SUBDIR)/wild_encounters.h +$(C_BUILDDIR)/trainer_see.o: c_dep += $(INCLUDE_DIRS)/constants/script_commands.h +$(C_BUILDDIR)/vs_seeker.o: c_dep += $(INCLUDE_DIRS)/constants/script_commands.h PERL := perl SHA1 := $(shell { command -v sha1sum || command -v shasum; } 2>/dev/null) -c diff --git a/include/battle_setup.h b/include/battle_setup.h index 1605345260..1323971f67 100644 --- a/include/battle_setup.h +++ b/include/battle_setup.h @@ -73,6 +73,7 @@ void ConfigureAndSetUpOneTrainerBattle(u8 trainerObjEventId, const u8 *trainerSc void ConfigureTwoTrainersBattle(u8 trainerObjEventId, const u8 *trainerScript); void SetUpTwoTrainersBattle(void); bool32 GetTrainerFlagFromScriptPointer(const u8 *data); +bool32 GetRematchFromScriptPointer(const u8 *data); void SetTrainerFacingDirection(void); u8 GetTrainerBattleMode(void); bool8 GetTrainerFlag(void); diff --git a/src/battle_setup.c b/src/battle_setup.c index a6d833c417..1103818c05 100644 --- a/src/battle_setup.c +++ b/src/battle_setup.c @@ -1125,6 +1125,13 @@ bool32 GetTrainerFlagFromScriptPointer(const u8 *data) TrainerBattleParameter *temp = (TrainerBattleParameter*)(data + OPCODE_OFFSET); return FlagGet(TRAINER_FLAGS_START + temp->params.opponentA); } + +bool32 GetRematchFromScriptPointer(const u8 *data) +{ + TrainerBattleParameter *temp = (TrainerBattleParameter*)(data + OPCODE_OFFSET); + return ShouldTryRematchBattleForTrainerId(temp->params.opponentA); +} + #undef OPCODE_OFFSET // Set trainer's movement type so they stop and remain facing that direction diff --git a/src/trainer_see.c b/src/trainer_see.c index 54ea4ebf22..2ec23dfa4f 100644 --- a/src/trainer_see.c +++ b/src/trainer_see.c @@ -19,6 +19,7 @@ #include "constants/event_objects.h" #include "constants/event_object_movement.h" #include "constants/field_effects.h" +#include "constants/script_commands.h" #include "constants/trainer_types.h" // this file's functions @@ -447,7 +448,7 @@ static u8 CheckTrainer(u8 objectEventId) struct ScriptContext ctx; if (RunScriptImmediatelyUntilEffect(SCREFF_V1 | SCREFF_SAVE | SCREFF_HARDWARE | SCREFF_TRAINERBATTLE, trainerBattlePtr, &ctx)) { - if (*ctx.scriptPtr == 0x5c) // trainerbattle + if (*ctx.scriptPtr == SCR_OP_TRAINERBATTLE) trainerBattlePtr = ctx.scriptPtr; else trainerBattlePtr = NULL; @@ -471,7 +472,18 @@ static u8 CheckTrainer(u8 objectEventId) else if (trainerBattlePtr) { if (GetTrainerFlagFromScriptPointer(trainerBattlePtr)) - return 0; + { + //If there is a rematch, we want to trigger the approach sequence + if (GetRematchFromScriptPointer(trainerBattlePtr)) + { + trainerBattlePtr = NULL; + numTrainers = 0xFF; + } + else + { + return 0; + } + } } else { diff --git a/src/vs_seeker.c b/src/vs_seeker.c index 8669ca3f47..1fcf5fdfcc 100644 --- a/src/vs_seeker.c +++ b/src/vs_seeker.c @@ -28,6 +28,7 @@ #include "constants/items.h" #include "constants/maps.h" #include "constants/songs.h" +#include "constants/script_commands.h" #include "constants/trainer_types.h" #include "constants/field_effects.h" @@ -718,12 +719,12 @@ static u16 GetTrainerFlagFromScript(const u8 *script) u16 trainerFlag; switch (script[0]) { - case 0x5c: + case SCR_OP_TRAINERBATTLE: script += 3; trainerFlag = script[0]; trainerFlag |= script[1] << 8; break; - case 0x23: + case SCR_OP_CALLNATIVE: u32 callnativeFunc = (((((script[4] << 8) + script[3]) << 8) + script[2]) << 8) + script[1]; if (callnativeFunc == ((u32)NativeVsSeekerRematchId | 0xA000000)) // | 0xA000000 corresponds to the request_effects=1 version of the function { diff --git a/tools/misc/make_scr_cmd_constants.py b/tools/misc/make_scr_cmd_constants.py new file mode 100644 index 0000000000..d158dfcf8a --- /dev/null +++ b/tools/misc/make_scr_cmd_constants.py @@ -0,0 +1,25 @@ +import re + +SCR_CMD_PAT = re.compile(r"\tscript_cmd_table_entry (\w+)\s+\w+,\s+[\w=]+\s+@( 0x[0-9a-f]+)") + +def main(): + output = [ + "//", + "// DO NOT MODIFY THIS FILE! It is auto-generated by tools/misc/make_scr_cmd_constants.py", + "//", + "#ifndef GUARD_SCR_CMD_CONSTANTS_H", + "#define GUARD_SCR_CMD_CONSTANTS_H\n", + ] + + with open("data/script_cmd_table.inc", "r") as f: + for line in f.readlines(): + if match := re.match(SCR_CMD_PAT, line): + new_line = "#define " + match.group(1) + match.group(2) + output.append(new_line) + + output.append("\n#endif // GUARD_SCR_CMD_CONSTANTS_H\n") + with open("include/constants/script_commands.h", "w+") as f: + f.write('\n'.join(output)) + +if __name__ == "__main__": + main() \ No newline at end of file From ff1039f148e2ab0a7da6757a4f5d9c3377c0fd64 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Fri, 7 Nov 2025 06:12:05 +0100 Subject: [PATCH 14/19] Fix grade in summary screen not accounting for 26 IV (#8157) --- src/pokemon_summary_screen.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/pokemon_summary_screen.c b/src/pokemon_summary_screen.c index a0722fc065..9016066db7 100644 --- a/src/pokemon_summary_screen.c +++ b/src/pokemon_summary_screen.c @@ -3800,18 +3800,18 @@ static const u8 *GetLetterGrade(u32 stat) static const u8 gText_GradeA[] = _("A"); static const u8 gText_GradeS[] = _("S"); - if (stat > 0 && stat <= 15) - return gText_GradeD; - else if (stat > 15 && stat <= 25) - return gText_GradeC; - else if (stat > 26 && stat <= 29) - return gText_GradeB; - else if (stat == 30) - return gText_GradeA; - else if (stat == 31) - return gText_GradeS; - else + if (stat <= 0) return gText_GradeF; + else if (stat <= 15) + return gText_GradeD; + else if (stat <= 25) + return gText_GradeC; + else if (stat <= 29) + return gText_GradeB; + else if (stat <= 30) + return gText_GradeA; + else + return gText_GradeS; } static void BufferLeftColumnStats(void) From cab4ec29da4685703feae0e73e63c3635663deb1 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Fri, 7 Nov 2025 10:05:50 +0100 Subject: [PATCH 15/19] Add additional comment explaing map name popup transparency side-effects (#8117) --- include/config/overworld.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/config/overworld.h b/include/config/overworld.h index cb110b4226..117d3b9eb5 100644 --- a/include/config/overworld.h +++ b/include/config/overworld.h @@ -133,6 +133,7 @@ #define OW_POPUP_BW_TIME_MODE OW_POPUP_BW_TIME_NONE // Determines what type of time is shown. #define OW_POPUP_BW_ALPHA_BLEND FALSE // Enables alpha blending/transparency for the pop-ups. Mainly intended to be used with the black color option. // Setting this to TRUE will cause graphical errors with the Day Night System enabled. + // It will also cause minor visual glitches of shadow and reflection sprites adjusting their transparency when the pop-up disappear // Pokémon Center #define OW_IGNORE_EGGS_ON_HEAL GEN_LATEST // In Gen 4+, the nurse in the Pokémon Center does not heal Eggs on healing machine. From 8d692bc96e1e9680a794b400d7c30744971d91b8 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Fri, 7 Nov 2025 11:46:56 +0100 Subject: [PATCH 16/19] Fix wild_encounters script not closing arrays properly (#8123) --- .../wild_encounters_to_header.py | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/tools/wild_encounters/wild_encounters_to_header.py b/tools/wild_encounters/wild_encounters_to_header.py index 5de52fbea9..375dda3ecf 100644 --- a/tools/wild_encounters/wild_encounters_to_header.py +++ b/tools/wild_encounters/wild_encounters_to_header.py @@ -391,35 +391,33 @@ def PrintWildMonHeadersContent(): timeCounter += 1 PrintEncounterHeaders(TabStr(2) + "},") PrintEncounterHeaders(tabStr + "},") - - if labelCount + 1 == headerStructTable[group][label]["encounterTotalCount"]: - PrintEncounterHeaders(tabStr + "{") - PrintEncounterHeaders(f"{TabStr(2)}.mapGroup = {GetMapGroupEnum(MAP_UNDEFINED)},") - PrintEncounterHeaders(f"{TabStr(2)}.mapNum = {GetMapGroupEnum(MAP_UNDEFINED, labelCount + 1)},") - - nullCount = 0 - while nullCount < TIMES_OF_DAY_COUNT: - if nullCount == 0: - PrintEncounterHeaders(f"{TabStr(2)}.encounterTypes =") - PrintEncounterHeaders(TabStr(2)+ "{") - - PrintEncounterHeaders(f"{TabStr(3)}[{TIME_OF_DAY.vals[nullCount]}] = ") - - nullIndex = 0 - while nullIndex <= len(fieldData) - 1: - if nullIndex == 0: - PrintEncounterHeaders(TabStr(3) + "{") - - PrintEncounterHeaders(f"{TabStr(4)}{GetIMonInfoStringFromIndex(nullIndex)} = NULL,") - - if nullIndex == len(fieldData) - 1: - PrintEncounterHeaders(TabStr(3) + "},") - - nullIndex += 1 - nullCount += 1 - PrintEncounterHeaders(TabStr(2) + "},") - PrintEncounterHeaders(tabStr + "},") labelCount += 1 + PrintEncounterHeaders(tabStr + "{") + PrintEncounterHeaders(f"{TabStr(2)}.mapGroup = {GetMapGroupEnum(MAP_UNDEFINED)},") + PrintEncounterHeaders(f"{TabStr(2)}.mapNum = {GetMapGroupEnum(MAP_UNDEFINED, labelCount + 1)},") + + nullCount = 0 + while nullCount < TIMES_OF_DAY_COUNT: + if nullCount == 0: + PrintEncounterHeaders(f"{TabStr(2)}.encounterTypes =") + PrintEncounterHeaders(TabStr(2)+ "{") + + PrintEncounterHeaders(f"{TabStr(3)}[{TIME_OF_DAY.vals[nullCount]}] = ") + + nullIndex = 0 + while nullIndex <= len(fieldData) - 1: + if nullIndex == 0: + PrintEncounterHeaders(TabStr(3) + "{") + + PrintEncounterHeaders(f"{TabStr(4)}{GetIMonInfoStringFromIndex(nullIndex)} = NULL,") + + if nullIndex == len(fieldData) - 1: + PrintEncounterHeaders(TabStr(3) + "},") + + nullIndex += 1 + nullCount += 1 + PrintEncounterHeaders(TabStr(2) + "},") + PrintEncounterHeaders(tabStr + "},") groupCount += 1 PrintEncounterHeaders("};") From 504b08dace4eb4d419392494ca9d807cf8d99304 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Fri, 7 Nov 2025 16:07:17 +0100 Subject: [PATCH 17/19] Fix transform not loading the correct sprites when facing shiny or unown (#8146) --- src/battle_gfx_sfx_util.c | 38 ++++++++++++------------------------ src/battle_script_commands.c | 5 ++++- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/battle_gfx_sfx_util.c b/src/battle_gfx_sfx_util.c index d7004d7635..1beaee4cc4 100644 --- a/src/battle_gfx_sfx_util.c +++ b/src/battle_gfx_sfx_util.c @@ -633,15 +633,8 @@ void BattleLoadMonSpriteGfx(struct Pokemon *mon, u32 battler) if (GetActiveGimmick(battler) == GIMMICK_DYNAMAX && GetMonData(mon, MON_DATA_GIGANTAMAX_FACTOR)) gBattleSpritesDataPtr->battlerData[battler].transformSpecies = species = GetGMaxTargetSpecies(species); - if (B_TRANSFORM_SHINY >= GEN_4) - { - personalityValue = gTransformedPersonalities[battler]; - isShiny = gTransformedShininess[battler]; - } - else - { - personalityValue = GetMonData(mon, MON_DATA_PERSONALITY); - } + personalityValue = gTransformedPersonalities[battler]; + isShiny = gTransformedShininess[battler]; } position = GetBattlerPosition(battler); @@ -947,24 +940,23 @@ void HandleSpeciesGfxDataChange(u8 battlerAtk, u8 battlerDef, bool32 megaEvo, bo targetSpecies = GetIllusionMonSpecies(battlerDef); else targetSpecies = GetMonData(monDef, MON_DATA_SPECIES); - personalityValue = GetMonData(monAtk, MON_DATA_PERSONALITY); - isShiny = GetMonData(monAtk, MON_DATA_IS_SHINY); + gBattleSpritesDataPtr->battlerData[battlerAtk].transformSpecies = targetSpecies; } else { targetSpecies = gBattleSpritesDataPtr->battlerData[battlerAtk].transformSpecies; - if (B_TRANSFORM_SHINY >= GEN_4 && trackEnemyPersonality && !megaEvo) - { - personalityValue = GetMonData(monDef, MON_DATA_PERSONALITY); - isShiny = GetMonData(monDef, MON_DATA_IS_SHINY); - } - else - { - personalityValue = GetMonData(monAtk, MON_DATA_PERSONALITY); - isShiny = GetMonData(monAtk, MON_DATA_IS_SHINY); - } } + if (trackEnemyPersonality) + { + personalityValue = gDisableStructs[battlerAtk].transformedMonPersonality; + isShiny = gDisableStructs[battlerAtk].transformedMonShininess; + } + else + { + personalityValue = GetMonData(monAtk, MON_DATA_PERSONALITY); + isShiny = GetMonData(monAtk, MON_DATA_IS_SHINY); + } HandleLoadSpecialPokePic(!IsOnPlayerSide(battlerAtk), gMonSpritesGfxPtr->spritesGfx[position], targetSpecies, @@ -981,10 +973,6 @@ void HandleSpeciesGfxDataChange(u8 battlerAtk, u8 battlerDef, bool32 megaEvo, bo { BlendPalette(paletteOffset, 16, 6, RGB_WHITE); CpuCopy32(&gPlttBufferFaded[paletteOffset], &gPlttBufferUnfaded[paletteOffset], PLTT_SIZEOF(16)); - if (!IsContest()) - { - gBattleSpritesDataPtr->battlerData[battlerAtk].transformSpecies = targetSpecies; - } } // dynamax tint diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index bc4e8d6551..f3a4cbd803 100755 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -11218,7 +11218,10 @@ static void Cmd_transformdataexecution(void) gDisableStructs[gBattlerAttacker].disabledMove = MOVE_NONE; gDisableStructs[gBattlerAttacker].disableTimer = 0; gDisableStructs[gBattlerAttacker].transformedMonPersonality = gBattleMons[gBattlerTarget].personality; - gDisableStructs[gBattlerAttacker].transformedMonShininess = gBattleMons[gBattlerTarget].isShiny; + if (B_TRANSFORM_SHINY >= GEN_4) + gDisableStructs[gBattlerAttacker].transformedMonShininess = gBattleMons[gBattlerTarget].isShiny; + else + gDisableStructs[gBattlerAttacker].transformedMonShininess = gBattleMons[gBattlerAttacker].isShiny; gDisableStructs[gBattlerAttacker].mimickedMoves = 0; gDisableStructs[gBattlerAttacker].usedMoves = 0; From 2467584af58d4aaae08877d8391ebc442557627e Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Fri, 7 Nov 2025 18:32:15 +0100 Subject: [PATCH 18/19] Prevent EXPECT functions from casting negative numbers into unsigned (#7866) --- include/test/test.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/test/test.h b/include/test/test.h index 48a6e84aa2..7b3225934d 100644 --- a/include/test/test.h +++ b/include/test/test.h @@ -143,7 +143,7 @@ s32 Test_MgbaPrintf(const char *fmt, ...); { \ typeof(a) _a = (a), _b = (b); \ if (_a != _b) \ - Test_ExitWithResult(TEST_RESULT_FAIL, __LINE__, ":L%s:%d: EXPECT_EQ(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \ + Test_ExitWithResult(TEST_RESULT_FAIL, __LINE__, ":L%s:%d: EXPECT_EQ(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, a, b); \ } while (0) #define EXPECT_NE(a, b) \ @@ -151,7 +151,7 @@ s32 Test_MgbaPrintf(const char *fmt, ...); { \ typeof(a) _a = (a), _b = (b); \ if (_a == _b) \ - Test_ExitWithResult(TEST_RESULT_FAIL, __LINE__, ":L%s:%d: EXPECT_NE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \ + Test_ExitWithResult(TEST_RESULT_FAIL, __LINE__, ":L%s:%d: EXPECT_NE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, a, b); \ } while (0) #define EXPECT_LT(a, b) \ @@ -159,7 +159,7 @@ s32 Test_MgbaPrintf(const char *fmt, ...); { \ typeof(a) _a = (a), _b = (b); \ if (_a >= _b) \ - Test_ExitWithResult(TEST_RESULT_FAIL, __LINE__, ":L%s:%d: EXPECT_LT(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \ + Test_ExitWithResult(TEST_RESULT_FAIL, __LINE__, ":L%s:%d: EXPECT_LT(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, a, b); \ } while (0) #define EXPECT_LE(a, b) \ @@ -167,7 +167,7 @@ s32 Test_MgbaPrintf(const char *fmt, ...); { \ typeof(a) _a = (a), _b = (b); \ if (_a > _b) \ - Test_ExitWithResult(TEST_RESULT_FAIL, __LINE__, ":L%s:%d: EXPECT_LE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \ + Test_ExitWithResult(TEST_RESULT_FAIL, __LINE__, ":L%s:%d: EXPECT_LE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, a, b); \ } while (0) #define EXPECT_GT(a, b) \ @@ -175,7 +175,7 @@ s32 Test_MgbaPrintf(const char *fmt, ...); { \ typeof(a) _a = (a), _b = (b); \ if (_a <= _b) \ - Test_ExitWithResult(TEST_RESULT_FAIL, __LINE__, ":L%s:%d: EXPECT_GT(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \ + Test_ExitWithResult(TEST_RESULT_FAIL, __LINE__, ":L%s:%d: EXPECT_GT(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, a, b); \ } while (0) #define EXPECT_GE(a, b) \ @@ -183,7 +183,7 @@ s32 Test_MgbaPrintf(const char *fmt, ...); { \ typeof(a) _a = (a), _b = (b); \ if (_a < _b) \ - Test_ExitWithResult(TEST_RESULT_FAIL, __LINE__, ":L%s:%d: EXPECT_GE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \ + Test_ExitWithResult(TEST_RESULT_FAIL, __LINE__, ":L%s:%d: EXPECT_GE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, a, b); \ } while (0) struct Benchmark { s32 ticks; }; From b8dc1968f3488fe45585712215a70bdb44b0c180 Mon Sep 17 00:00:00 2001 From: PhallenTree <168426989+PhallenTree@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:23:00 +0000 Subject: [PATCH 19/19] Fixes Receiver not immediately activating copied Soul Heart (#8162) --- data/battle_scripts_1.s | 5 +-- test/battle/ability/receiver.c | 59 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 49768b2c43..b47a683f2c 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -5085,8 +5085,8 @@ BattleScript_FaintAttacker:: tryrevertweatherform flushtextbox waitanimation - tryactivatesoulheart tryactivatereceiver BS_ATTACKER + tryactivatesoulheart trytrainerslidemsgfirstoff BS_ATTACKER return @@ -5103,8 +5103,8 @@ BattleScript_FaintTarget:: tryrevertweatherform flushtextbox waitanimation - tryactivatesoulheart tryactivatereceiver BS_TARGET + tryactivatesoulheart trytrainerslidemsgfirstoff BS_TARGET return @@ -7285,6 +7285,7 @@ BattleScript_ReceiverActivates:: printstring STRINGID_RECEIVERABILITYTAKEOVER waitmessage B_WAIT_TIME_LONG settracedability BS_ABILITY_BATTLER + switchinabilities BS_ABILITY_BATTLER return BattleScript_AbilityHpHeal: diff --git a/test/battle/ability/receiver.c b/test/battle/ability/receiver.c index a9155b7244..7e6b7076ba 100644 --- a/test/battle/ability/receiver.c +++ b/test/battle/ability/receiver.c @@ -1,4 +1,63 @@ #include "global.h" #include "test/battle.h" +DOUBLE_BATTLE_TEST("Receiver copies ally's ability when they faint and immediately activates it") +{ + GIVEN { + ASSUME(!gAbilitiesInfo[ABILITY_INTIMIDATE].cantBeCopied); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_PASSIMIAN) { Ability(ABILITY_RECEIVER); } + OPPONENT(SPECIES_GYARADOS) { Ability(ABILITY_INTIMIDATE); HP(1); } + } WHEN { + TURN { MOVE(playerLeft, MOVE_SCRATCH, target: opponentRight); } + } SCENE { + ABILITY_POPUP(opponentRight, ABILITY_INTIMIDATE); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, playerLeft); + ABILITY_POPUP(opponentLeft, ABILITY_RECEIVER); + ABILITY_POPUP(opponentLeft, ABILITY_INTIMIDATE); + } THEN { + EXPECT_EQ(playerLeft->statStages[STAT_ATK], DEFAULT_STAT_STAGE - 2); + EXPECT_EQ(playerRight->statStages[STAT_ATK], DEFAULT_STAT_STAGE - 2); + } +} + +DOUBLE_BATTLE_TEST("Receiver copies ally's ability when they faint and can activate it on future moves") +{ + GIVEN { + ASSUME(!gAbilitiesInfo[ABILITY_WATER_ABSORB].cantBeCopied); + ASSUME(GetMoveType(MOVE_WATER_GUN) == TYPE_WATER); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_PASSIMIAN) { Ability(ABILITY_RECEIVER); } + OPPONENT(SPECIES_LANTURN) { Ability(ABILITY_WATER_ABSORB); HP(1); } + } WHEN { + TURN { MOVE(playerLeft, MOVE_SCRATCH, target: opponentRight); MOVE(playerRight, MOVE_WATER_GUN, target: opponentLeft); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, playerLeft); + ABILITY_POPUP(opponentLeft, ABILITY_RECEIVER); + ABILITY_POPUP(opponentLeft, ABILITY_WATER_ABSORB); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_WATER_GUN, playerRight); + } +} + +DOUBLE_BATTLE_TEST("Receiver copies ally's Soul Heart and immediately activates it") +{ + GIVEN { + ASSUME(!gAbilitiesInfo[ABILITY_SOUL_HEART].cantBeCopied); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_PASSIMIAN) { Ability(ABILITY_RECEIVER); } + OPPONENT(SPECIES_MAGEARNA) { Ability(ABILITY_SOUL_HEART); HP(1); } + } WHEN { + TURN { MOVE(playerLeft, MOVE_SCRATCH, target: opponentRight); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, playerLeft); + ABILITY_POPUP(opponentLeft, ABILITY_RECEIVER); + ABILITY_POPUP(opponentLeft, ABILITY_SOUL_HEART); + } THEN { + EXPECT_EQ(opponentLeft->statStages[STAT_SPATK], DEFAULT_STAT_STAGE + 1); + } +} + TO_DO_BATTLE_TEST("TODO: Write Receiver (Ability) test titles")