diff --git a/data/battle_scripts_2.s b/data/battle_scripts_2.s index 5b0827ade62b..e8e9358771ff 100644 --- a/data/battle_scripts_2.s +++ b/data/battle_scripts_2.s @@ -153,6 +153,7 @@ BattleScript_PrintCaughtMonInfo:: getexp BS_TARGET sethword gBattle_BG2_X, 0 BattleScript_TryPrintCaughtMonInfo: + jumpifbattletype BATTLE_TYPE_RECORDED, BattleScript_GiveCaughtMonEnd trysetcaughtmondexflags BattleScript_TryNicknameCaughtMon printstring STRINGID_PKMNDATAADDEDTODEX waitstate diff --git a/include/battle_controllers.h b/include/battle_controllers.h index 4e3ab7eb05c3..199b6c920737 100644 --- a/include/battle_controllers.h +++ b/include/battle_controllers.h @@ -304,6 +304,7 @@ void BtlController_HandleBattleAnimation(u32 battler, bool32 ignoreSE, bool32 up // player controller void SetControllerToPlayer(u32 battler); void SetBattleEndCallbacks(u32 battler); +void PlayerHandleBallThrowAnim(u32 battler); void PlayerHandleExpUpdate(u32 battler); u32 LinkPlayerGetTrainerPicId(u32 multiplayerId); void CB2_SetUpReshowBattleScreenAfterMenu(void); diff --git a/include/constants/battle.h b/include/constants/battle.h index 0f0d39db8d5b..6a2a56405981 100644 --- a/include/constants/battle.h +++ b/include/constants/battle.h @@ -86,6 +86,7 @@ | BATTLE_TYPE_GROUDON | BATTLE_TYPE_KYOGRE | BATTLE_TYPE_RAYQUAZA)) #define WILD_DOUBLE_BATTLE ((gBattleTypeFlags & BATTLE_TYPE_DOUBLE && !(gBattleTypeFlags & (BATTLE_TYPE_LINK | BATTLE_TYPE_TRAINER)))) +#define RECORDED_WILD_BATTLE ((gBattleTypeFlags & BATTLE_TYPE_RECORDED) && !(gBattleTypeFlags & (BATTLE_TYPE_TRAINER | BATTLE_TYPE_FRONTIER))) #define BATTLE_TWO_VS_ONE_OPPONENT ((gBattleTypeFlags & BATTLE_TYPE_INGAME_PARTNER && gTrainerBattleOpponent_B == 0xFFFF)) #define BATTLE_TYPE_HAS_AI (BATTLE_TYPE_TRAINER | BATTLE_TYPE_FIRST_BATTLE | BATTLE_TYPE_SAFARI | BATTLE_TYPE_ROAMER | BATTLE_TYPE_INGAME_PARTNER) diff --git a/include/recorded_battle.h b/include/recorded_battle.h index e4fa1deb406a..fcbe9495fd86 100644 --- a/include/recorded_battle.h +++ b/include/recorded_battle.h @@ -65,6 +65,7 @@ u8 RecordedBattle_BufferNewBattlerData(u8 *dst); void RecordedBattle_RecordAllBattlerData(u8 *data); bool32 CanCopyRecordedBattleSaveData(void); bool32 MoveRecordedBattleToSaveData(void); +void SetPartiesFromRecordedSave(struct RecordedBattleSave *src); void SetVariablesForRecordedBattle(struct RecordedBattleSave *); void PlayRecordedBattle(void (*CB2_After)(void)); u8 GetRecordedBattleFrontierFacility(void); diff --git a/include/test/battle.h b/include/test/battle.h index e1b00efbdd20..1e264f95bdf1 100644 --- a/include/test/battle.h +++ b/include/test/battle.h @@ -359,6 +359,20 @@ * ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); * target can only be specified for ANIM_TYPE_MOVE. * + * EXPERIENCE_BAR(battler, [exp: | captureGainedExp:]) + * If exp: is used, causes the test to fail if that amount of + * experience is not gained, e.g.: + * EXPERIENCE_BAR(player, exp: 0); + * If captureGainedExp: is used, causes the test to fail if + * the Experience bar does not change, and then writes that change to the + * pointer, e.g.: + * u32 exp; + * EXPERIENCE_BAR(player, captureGainedExp: &exp); + * If none of the above are used, causes the test to fail if the Exp + * does not change at all. + * Please note that due to nature of tests, this command + * is only usable in WILD_BATTLE_TEST and will fail elsewhere. + * * HP_BAR(battler, [damage: | hp: | captureDamage: | captureHP:]) * If hp: or damage: are used, causes the test to fail if that amount of * damage is not dealt, e.g.: @@ -470,7 +484,7 @@ #define MAX_TURNS 16 #define MAX_QUEUED_EVENTS 25 -enum { BATTLE_TEST_SINGLES, BATTLE_TEST_DOUBLES }; +enum { BATTLE_TEST_SINGLES, BATTLE_TEST_DOUBLES, BATTLE_TEST_WILD }; typedef void (*SingleBattleTestFunction)(void *, u32, struct BattlePokemon *, struct BattlePokemon *); typedef void (*DoubleBattleTestFunction)(void *, u32, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *); @@ -492,6 +506,7 @@ enum QUEUED_ABILITY_POPUP_EVENT, QUEUED_ANIMATION_EVENT, QUEUED_HP_EVENT, + QUEUED_EXP_EVENT, QUEUED_MESSAGE_EVENT, QUEUED_STATUS_EVENT, }; @@ -511,6 +526,7 @@ struct QueuedAnimationEvent }; enum { HP_EVENT_NEW_HP, HP_EVENT_DELTA_HP }; +enum { EXP_EVENT_NEW_EXP, EXP_EVENT_DELTA_EXP }; struct QueuedHPEvent { @@ -519,6 +535,13 @@ struct QueuedHPEvent u32 address:28; }; +struct QueuedExpEvent +{ + u32 battlerId:3; + u32 type:1; + u32 address:28; +}; + struct QueuedMessageEvent { const u8 *pattern; @@ -541,6 +564,7 @@ struct QueuedEvent struct QueuedAbilityEvent ability; struct QueuedAnimationEvent animation; struct QueuedHPEvent hp; + struct QueuedExpEvent exp; struct QueuedMessageEvent message; struct QueuedStatusEvent status; } as; @@ -676,6 +700,24 @@ extern struct BattleTestRunnerState *gBattleTestRunnerState; }; \ static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *results, u32 i, struct BattlePokemon *player, struct BattlePokemon *opponent) +#define WILD_BATTLE_TEST(_name, ...) \ + struct CAT(Result, __LINE__) { MEMBERS(__VA_ARGS__) }; \ + static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *, u32, struct BattlePokemon *, struct BattlePokemon *); \ + __attribute__((section(".tests"))) static const struct Test CAT(sTest, __LINE__) = \ + { \ + .name = _name, \ + .filename = __FILE__, \ + .runner = &gBattleTestRunner, \ + .data = (void *)&(const struct BattleTest) \ + { \ + .type = BATTLE_TEST_WILD, \ + .sourceLine = __LINE__, \ + .function = { .singles = (SingleBattleTestFunction)CAT(Test, __LINE__) }, \ + .resultsSize = sizeof(struct CAT(Result, __LINE__)), \ + }, \ + }; \ + static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *results, u32 i, struct BattlePokemon *player, struct BattlePokemon *opponent) + #define DOUBLE_BATTLE_TEST(_name, ...) \ struct CAT(Result, __LINE__) { MEMBERS(__VA_ARGS__) }; \ static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *, u32, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *); \ @@ -741,6 +783,7 @@ struct moveWithPP { #define MovesWithPP(movewithpp1, ...) MovesWithPP_(__LINE__, (struct moveWithPP[MAX_MON_MOVES]) {movewithpp1, __VA_ARGS__}) #define Friendship(friendship) Friendship_(__LINE__, friendship) #define Status1(status1) Status1_(__LINE__, status1) +#define OTName(otName) do {static const u8 otName_[] = _(otName); OTName_(__LINE__, otName_);} while (0) void OpenPokemon(u32 sourceLine, u32 side, u32 species); void ClosePokemon(u32 sourceLine); @@ -762,6 +805,7 @@ void Moves_(u32 sourceLine, const u16 moves[MAX_MON_MOVES]); void MovesWithPP_(u32 sourceLine, struct moveWithPP moveWithPP[MAX_MON_MOVES]); void Friendship_(u32 sourceLine, u32 friendship); void Status1_(u32 sourceLine, u32 status1); +void OTName_(u32 sourceLine, const u8 *otName); #define PLAYER_PARTY (gBattleTestRunnerState->data.recordedBattle.playerParty) #define OPPONENT_PARTY (gBattleTestRunnerState->data.recordedBattle.opponentParty) @@ -837,6 +881,7 @@ void SendOut(u32 sourceLine, struct BattlePokemon *, u32 partyIndex); #define ABILITY_POPUP(battler, ...) QueueAbility(__LINE__, battler, (struct AbilityEventContext) { __VA_ARGS__ }) #define ANIMATION(type, id, ...) QueueAnimation(__LINE__, type, id, (struct AnimationEventContext) { __VA_ARGS__ }) #define HP_BAR(battler, ...) QueueHP(__LINE__, battler, (struct HPEventContext) { APPEND_TRUE(__VA_ARGS__) }) +#define EXPERIENCE_BAR(battler, ...) QueueExp(__LINE__, battler, (struct ExpEventContext) { APPEND_TRUE(__VA_ARGS__) }) // Static const is needed to make the modern compiler put the pattern variable in the .rodata section, instead of putting it on stack(which can break the game). #define MESSAGE(pattern) do {static const u8 msg[] = _(pattern); QueueMessage(__LINE__, msg);} while (0) #define STATUS_ICON(battler, status) QueueStatus(__LINE__, battler, (struct StatusEventContext) { status }) @@ -872,6 +917,15 @@ struct HPEventContext bool8 explicitCaptureDamage; }; +struct ExpEventContext +{ + u8 _; + u32 exp; + bool8 explicitExp; + s32 *captureGainedExp; + bool8 explicitCaptureGainedExp; +}; + struct StatusEventContext { u16 status1; @@ -891,6 +945,7 @@ void CloseQueueGroup(u32 sourceLine); void QueueAbility(u32 sourceLine, struct BattlePokemon *battler, struct AbilityEventContext); void QueueAnimation(u32 sourceLine, u32 type, u32 id, struct AnimationEventContext); void QueueHP(u32 sourceLine, struct BattlePokemon *battler, struct HPEventContext); +void QueueExp(u32 sourceLine, struct BattlePokemon *battler, struct ExpEventContext); void QueueMessage(u32 sourceLine, const u8 *pattern); void QueueStatus(u32 sourceLine, struct BattlePokemon *battler, struct StatusEventContext); diff --git a/include/test_runner.h b/include/test_runner.h index fddd4d656148..199b2e6cb137 100644 --- a/include/test_runner.h +++ b/include/test_runner.h @@ -10,6 +10,7 @@ extern const bool8 gTestRunnerSkipIsFail; void TestRunner_Battle_RecordAbilityPopUp(u32 battlerId, u32 ability); void TestRunner_Battle_RecordAnimation(u32 animType, u32 animId); void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP); +void TestRunner_Battle_RecordExp(u32 battlerId, u32 oldExp, u32 newExp); void TestRunner_Battle_RecordMessage(const u8 *message); void TestRunner_Battle_RecordStatus1(u32 battlerId, u32 status1); void TestRunner_Battle_AfterLastTurn(void); @@ -23,6 +24,7 @@ u32 TestRunner_Battle_GetForcedAbility(u32 side, u32 partyIndex); #define TestRunner_Battle_RecordAbilityPopUp(...) (void)0 #define TestRunner_Battle_RecordAnimation(...) (void)0 #define TestRunner_Battle_RecordHP(...) (void)0 +#define TestRunner_Battle_RecordExp(...) (void)0 #define TestRunner_Battle_RecordMessage(...) (void)0 #define TestRunner_Battle_RecordStatus1(...) (void)0 #define TestRunner_Battle_AfterLastTurn(...) (void)0 diff --git a/src/battle_controller_player.c b/src/battle_controller_player.c index 3a464910e0c9..83d3a8525346 100644 --- a/src/battle_controller_player.c +++ b/src/battle_controller_player.c @@ -26,6 +26,7 @@ #include "sound.h" #include "string_util.h" #include "task.h" +#include "test_runner.h" #include "text.h" #include "util.h" #include "window.h" @@ -45,7 +46,6 @@ static void PlayerHandleTrainerSlide(u32 battler); static void PlayerHandleTrainerSlideBack(u32 battler); static void PlayerHandlePaletteFade(u32 battler); static void PlayerHandleSuccessBallThrowAnim(u32 battler); -static void PlayerHandleBallThrowAnim(u32 battler); static void PlayerHandlePause(u32 battler); static void PlayerHandleMoveAnimation(u32 battler); static void PlayerHandlePrintString(u32 battler); @@ -1452,6 +1452,7 @@ static void Task_PrepareToGiveExpWithExpBar(u8 taskId) exp -= currLvlExp; expToNextLvl = gExperienceTables[gSpeciesInfo[species].growthRate][level + 1] - currLvlExp; SetBattleBarStruct(battler, gHealthboxSpriteIds[battler], expToNextLvl, exp, -gainedExp); + TestRunner_Battle_RecordExp(battler, exp, -gainedExp); PlaySE(SE_EXP); gTasks[taskId].func = Task_GiveExpWithExpBar; } @@ -1850,7 +1851,7 @@ static void PlayerHandleSuccessBallThrowAnim(u32 battler) BtlController_HandleSuccessBallThrowAnim(battler, gBattlerTarget, B_ANIM_BALL_THROW, TRUE); } -static void PlayerHandleBallThrowAnim(u32 battler) +void PlayerHandleBallThrowAnim(u32 battler) { BtlController_HandleBallThrowAnim(battler, gBattlerTarget, B_ANIM_BALL_THROW, TRUE); } diff --git a/src/battle_controller_recorded_opponent.c b/src/battle_controller_recorded_opponent.c index 2443b56f7445..11f0fec8c51d 100644 --- a/src/battle_controller_recorded_opponent.c +++ b/src/battle_controller_recorded_opponent.c @@ -501,27 +501,7 @@ static void RecordedOpponentHandleChoosePokemon(u32 battler) static void RecordedOpponentHandleHealthBarUpdate(u32 battler) { - s16 hpVal; - s32 maxHP, curHP; - - LoadBattleBarGfx(0); - hpVal = gBattleResources->bufferA[battler][2] | (gBattleResources->bufferA[battler][3] << 8); - - maxHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[battler]], MON_DATA_MAX_HP); - curHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[battler]], MON_DATA_HP); - - if (hpVal != INSTANT_HP_BAR_DROP) - { - SetBattleBarStruct(battler, gHealthboxSpriteIds[battler], maxHP, curHP, hpVal); - TestRunner_Battle_RecordHP(battler, curHP, min(maxHP, max(0, curHP - hpVal))); - } - else - { - SetBattleBarStruct(battler, gHealthboxSpriteIds[battler], maxHP, 0, hpVal); - TestRunner_Battle_RecordHP(battler, curHP, 0); - } - - gBattlerControllerFuncs[battler] = Controller_WaitForHealthBar; + BtlController_HandleHealthBarUpdate(battler, FALSE); } static void RecordedOpponentHandleStatusIconUpdate(u32 battler) diff --git a/src/battle_controller_recorded_player.c b/src/battle_controller_recorded_player.c index 9c8acdbd1747..f372c0ff8881 100644 --- a/src/battle_controller_recorded_player.c +++ b/src/battle_controller_recorded_player.c @@ -66,7 +66,7 @@ static void (*const sRecordedPlayerBufferCommands[CONTROLLER_CMDS_COUNT])(u32 ba [CONTROLLER_FAINTANIMATION] = BtlController_HandleFaintAnimation, [CONTROLLER_PALETTEFADE] = BtlController_Empty, [CONTROLLER_SUCCESSBALLTHROWANIM] = BtlController_Empty, - [CONTROLLER_BALLTHROWANIM] = BtlController_Empty, + [CONTROLLER_BALLTHROWANIM] = PlayerHandleBallThrowAnim, [CONTROLLER_PAUSE] = BtlController_Empty, [CONTROLLER_MOVEANIMATION] = RecordedPlayerHandleMoveAnimation, [CONTROLLER_PRINTSTRING] = RecordedPlayerHandlePrintString, @@ -78,7 +78,7 @@ static void (*const sRecordedPlayerBufferCommands[CONTROLLER_CMDS_COUNT])(u32 ba [CONTROLLER_CHOOSEPOKEMON] = RecordedPlayerHandleChoosePokemon, [CONTROLLER_23] = BtlController_Empty, [CONTROLLER_HEALTHBARUPDATE] = RecordedPlayerHandleHealthBarUpdate, - [CONTROLLER_EXPUPDATE] = BtlController_Empty, + [CONTROLLER_EXPUPDATE] = PlayerHandleExpUpdate, [CONTROLLER_STATUSICONUPDATE] = RecordedPlayerHandleStatusIconUpdate, [CONTROLLER_STATUSANIMATION] = RecordedPlayerHandleStatusAnimation, [CONTROLLER_STATUSXOR] = BtlController_Empty, @@ -507,28 +507,7 @@ static void RecordedPlayerHandleChoosePokemon(u32 battler) static void RecordedPlayerHandleHealthBarUpdate(u32 battler) { - s16 hpVal; - s32 maxHP, curHP; - - LoadBattleBarGfx(0); - hpVal = gBattleResources->bufferA[battler][2] | (gBattleResources->bufferA[battler][3] << 8); - - maxHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[battler]], MON_DATA_MAX_HP); - curHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[battler]], MON_DATA_HP); - - if (hpVal != INSTANT_HP_BAR_DROP) - { - SetBattleBarStruct(battler, gHealthboxSpriteIds[battler], maxHP, curHP, hpVal); - TestRunner_Battle_RecordHP(battler, curHP, min(maxHP, max(0, curHP - hpVal))); - } - else - { - SetBattleBarStruct(battler, gHealthboxSpriteIds[battler], maxHP, 0, hpVal); - UpdateHpTextInHealthbox(gHealthboxSpriteIds[battler], HP_CURRENT, 0, maxHP); - TestRunner_Battle_RecordHP(battler, curHP, 0); - } - - gBattlerControllerFuncs[battler] = Controller_WaitForHealthBar; + BtlController_HandleHealthBarUpdate(battler, TRUE); } static void RecordedPlayerHandleStatusIconUpdate(u32 battler) diff --git a/src/battle_controllers.c b/src/battle_controllers.c index 5deadd26edfc..026f433720a1 100644 --- a/src/battle_controllers.c +++ b/src/battle_controllers.c @@ -19,6 +19,7 @@ #include "string_util.h" #include "sound.h" #include "task.h" +#include "test_runner.h" #include "util.h" #include "text.h" #include "constants/abilities.h" @@ -2687,26 +2688,26 @@ void BtlController_HandlePrintString(u32 battler, bool32 updateTvData, bool32 ar void BtlController_HandleHealthBarUpdate(u32 battler, bool32 updateHpText) { + s32 maxHP, curHP; s16 hpVal; struct Pokemon *party = GetBattlerParty(battler); LoadBattleBarGfx(0); hpVal = gBattleResources->bufferA[battler][2] | (gBattleResources->bufferA[battler][3] << 8); + maxHP = GetMonData(&party[gBattlerPartyIndexes[battler]], MON_DATA_MAX_HP); + curHP = GetMonData(&party[gBattlerPartyIndexes[battler]], MON_DATA_HP); if (hpVal != INSTANT_HP_BAR_DROP) { - u32 maxHP = GetMonData(&party[gBattlerPartyIndexes[battler]], MON_DATA_MAX_HP); - u32 curHP = GetMonData(&party[gBattlerPartyIndexes[battler]], MON_DATA_HP); - SetBattleBarStruct(battler, gHealthboxSpriteIds[battler], maxHP, curHP, hpVal); + TestRunner_Battle_RecordHP(battler, curHP, min(maxHP, max(0, curHP - hpVal))); } else { - u32 maxHP = GetMonData(&party[gBattlerPartyIndexes[battler]], MON_DATA_MAX_HP); - SetBattleBarStruct(battler, gHealthboxSpriteIds[battler], maxHP, 0, hpVal); if (updateHpText) UpdateHpTextInHealthbox(gHealthboxSpriteIds[battler], HP_CURRENT, 0, maxHP); + TestRunner_Battle_RecordHP(battler, curHP, 0); } gBattlerControllerFuncs[battler] = Controller_WaitForHealthBar; diff --git a/src/battle_message.c b/src/battle_message.c index a1c862beb473..d80ae537e8ae 100644 --- a/src/battle_message.c +++ b/src/battle_message.c @@ -3865,7 +3865,7 @@ void BattlePutTextOnWindow(const u8 *text, u8 windowId) else gTextFlags.useAlternateDownArrow = TRUE; - if (gBattleTypeFlags & (BATTLE_TYPE_LINK | BATTLE_TYPE_RECORDED)) + if ((gBattleTypeFlags & (BATTLE_TYPE_LINK | BATTLE_TYPE_RECORDED)) || gTestRunnerEnabled) gTextFlags.autoScroll = TRUE; else gTextFlags.autoScroll = FALSE; diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 588150e5e255..ac0f42fca645 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -4030,6 +4030,23 @@ static void Cmd_jumpbasedontype(void) FEATURE_FLAG_ASSERT(I_EXP_SHARE_FLAG, YouNeedToSetTheExpShareFlagToAnUnusedFlag); +static bool32 BattleTypeAllowsExp(void) +{ + if (RECORDED_WILD_BATTLE) + return TRUE; + else if (gBattleTypeFlags & + ( BATTLE_TYPE_LINK + | BATTLE_TYPE_RECORDED_LINK + | BATTLE_TYPE_TRAINER_HILL + | BATTLE_TYPE_FRONTIER + | BATTLE_TYPE_SAFARI + | BATTLE_TYPE_BATTLE_TOWER + | BATTLE_TYPE_EREADER_TRAINER)) + return FALSE; + else + return TRUE; +} + static u32 GetMonHoldEffect(struct Pokemon *mon) { u32 holdEffect; @@ -4058,14 +4075,7 @@ static void Cmd_getexp(void) case 0: // check if should receive exp at all if (GetBattlerSide(gBattlerFainted) != B_SIDE_OPPONENT || IsAiVsAiBattle() - || (gBattleTypeFlags & - (BATTLE_TYPE_LINK - | BATTLE_TYPE_RECORDED_LINK - | BATTLE_TYPE_TRAINER_HILL - | BATTLE_TYPE_FRONTIER - | BATTLE_TYPE_SAFARI - | BATTLE_TYPE_BATTLE_TOWER - | BATTLE_TYPE_EREADER_TRAINER))) + || !BattleTypeAllowsExp()) { gBattleScripting.getexpState = 6; // goto last case } @@ -7086,7 +7096,7 @@ static void Cmd_handlelearnnewmove(void) while (learnMove == MON_ALREADY_KNOWS_MOVE) learnMove = MonTryLearningNewMove(&gPlayerParty[monId], FALSE); - if (learnMove == MOVE_NONE) + if (learnMove == MOVE_NONE || RECORDED_WILD_BATTLE) { gBattlescriptCurrInstr = cmd->nothingToLearnPtr; } @@ -7777,7 +7787,7 @@ static void Cmd_drawlvlupbox(void) } break; case 6: - if (gMain.newKeys != 0) + if (gMain.newKeys != 0 || RECORDED_WILD_BATTLE) { // Draw page 2 of level up box PlaySE(SE_SELECT); @@ -7787,7 +7797,7 @@ static void Cmd_drawlvlupbox(void) } break; case 8: - if (gMain.newKeys != 0) + if (gMain.newKeys != 0 || RECORDED_WILD_BATTLE) { // Close level up box PlaySE(SE_SELECT); @@ -15264,7 +15274,7 @@ static void Cmd_trysetcaughtmondexflags(void) { CMD_ARGS(const u8 *failInstr); - u16 species = GetMonData(&gEnemyParty[gBattlerPartyIndexes[GetCatchingBattler()]], MON_DATA_SPECIES, NULL); + u32 species = GetMonData(&gEnemyParty[gBattlerPartyIndexes[GetCatchingBattler()]], MON_DATA_SPECIES, NULL); u32 personality = GetMonData(&gEnemyParty[gBattlerPartyIndexes[GetCatchingBattler()]], MON_DATA_PERSONALITY, NULL); if (GetSetPokedexFlag(SpeciesToNationalPokedexNum(species), FLAG_GET_CAUGHT)) diff --git a/src/recorded_battle.c b/src/recorded_battle.c index 17f12ce93c05..455ff17ff138 100644 --- a/src/recorded_battle.c +++ b/src/recorded_battle.c @@ -493,20 +493,25 @@ static void Task_StartAfterCountdown(u8 taskId) } } -void SetVariablesForRecordedBattle(struct RecordedBattleSave *src) +void SetPartiesFromRecordedSave(struct RecordedBattleSave *src) { - bool8 var; - s32 i, j; + s32 i; ZeroPlayerPartyMons(); ZeroEnemyPartyMons(); - for (i = 0; i < PARTY_SIZE; i++) { gPlayerParty[i] = src->playerParty[i]; gEnemyParty[i] = src->opponentParty[i]; } +} + +void SetVariablesForRecordedBattle(struct RecordedBattleSave *src) +{ + bool8 var; + s32 i, j; + SetPartiesFromRecordedSave(src); for (i = 0; i < MAX_BATTLERS_COUNT; i++) { for (var = FALSE, j = 0; j < PLAYER_NAME_LENGTH + 1; j++) diff --git a/test/battle/exp.c b/test/battle/exp.c new file mode 100644 index 000000000000..e01055a9be1a --- /dev/null +++ b/test/battle/exp.c @@ -0,0 +1,150 @@ +#include "global.h" +#include "test/battle.h" + +#if B_EXP_CATCH >= GEN_6 + +WILD_BATTLE_TEST("Pokemon gain exp after catching a Pokemon") +{ + u8 level = 0; + + PARAMETRIZE { level = 50; } + PARAMETRIZE { level = MAX_LEVEL; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Level(level); } + OPPONENT(SPECIES_CATERPIE) { HP(1); } + } WHEN { + TURN { USE_ITEM(player, ITEM_ULTRA_BALL); } + } SCENE { + MESSAGE("You used Ultra Ball!"); + ANIMATION(ANIM_TYPE_SPECIAL, B_ANIM_BALL_THROW, player); + if (level != MAX_LEVEL) { + EXPERIENCE_BAR(player); + } + } +} + +#endif // B_EXP_CATCH + +WILD_BATTLE_TEST("Higher leveled Pokemon give more exp", u32 exp) +{ + u8 level = 0; + + PARAMETRIZE { level = 5; } + PARAMETRIZE { level = 10; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Level(20); } + OPPONENT(SPECIES_CATERPIE) { Level(level); HP(1); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); } + } SCENE { + MESSAGE("Wobbuffet used Tackle!"); + MESSAGE("Wild Caterpie fainted!"); + EXPERIENCE_BAR(player, captureGainedExp: &results[i].exp); + } FINALLY { + EXPECT_GT(results[1].exp, results[0].exp); + } +} + +WILD_BATTLE_TEST("Lucky Egg boosts gained exp points by 50%", u32 exp) +{ + u32 item = 0; + + PARAMETRIZE { item = ITEM_LUCKY_EGG; } + PARAMETRIZE { item = ITEM_NONE; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Level(20); Item(item); } + OPPONENT(SPECIES_CATERPIE) { Level(10); HP(1); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); } + } SCENE { + MESSAGE("Wobbuffet used Tackle!"); + MESSAGE("Wild Caterpie fainted!"); + EXPERIENCE_BAR(player, captureGainedExp: &results[i].exp); + } FINALLY { + EXPECT_MUL_EQ(results[1].exp, Q_4_12(1.5), results[0].exp); + } +} + +#if (B_SCALED_EXP == GEN_5 || B_SCALED_EXP >= GEN_7) + +WILD_BATTLE_TEST("Exp is scaled to player and opponent's levels", u32 exp) +{ + u8 level = 0; + + PARAMETRIZE { level = 5; } + PARAMETRIZE { level = 10; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Level(level); } + OPPONENT(SPECIES_CATERPIE) { Level(5); HP(1); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); } + } SCENE { + MESSAGE("Wobbuffet used Tackle!"); + MESSAGE("Wild Caterpie fainted!"); + EXPERIENCE_BAR(player, captureGainedExp: &results[i].exp); + } FINALLY { + EXPECT_GT(results[0].exp, results[1].exp); + } +} + +#endif + +WILD_BATTLE_TEST("Large exp gains are supported", u32 exp) // #1455 +{ + u8 level = 0; + + PARAMETRIZE { level = 10; } + PARAMETRIZE { level = 50; } + PARAMETRIZE { level = MAX_LEVEL; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Level(1); Item(ITEM_LUCKY_EGG); OTName("Test"); } // OT Name is different so it gets more exp as a traded mon + OPPONENT(SPECIES_BLISSEY) { Level(level); HP(1); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); } + } SCENE { + MESSAGE("Wobbuffet used Tackle!"); + MESSAGE("Wild Blissey fainted!"); + EXPERIENCE_BAR(player, captureGainedExp: &results[i].exp); + } THEN { + EXPECT(GetMonData(&gPlayerParty[0], MON_DATA_LEVEL) > 1); + EXPECT(GetMonData(&gPlayerParty[0], MON_DATA_EXP) > 1); + } FINALLY { + EXPECT_GT(results[1].exp, results[0].exp); + EXPECT_GT(results[2].exp, results[1].exp); + } +} + +#if I_EXP_SHARE_ITEM < GEN_6 + +WILD_BATTLE_TEST("Exp Share(held) gives Experience to mons which did not participate in battle") +{ + u32 item = 0; + + PARAMETRIZE { item = ITEM_NONE; } + PARAMETRIZE { item = ITEM_EXP_SHARE; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT) { Level(40); Item(item); } + OPPONENT(SPECIES_CATERPIE) { Level(10); HP(1); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); } + } SCENE { + MESSAGE("Wobbuffet used Tackle!"); + MESSAGE("Wild Caterpie fainted!"); + // This message should appear only for gen6> exp share. + NOT MESSAGE("The rest of your team gained EXP. Points thanks to the Exp. Share!"); + } THEN { + if (item == ITEM_EXP_SHARE) + EXPECT_GT(GetMonData(&gPlayerParty[1], MON_DATA_EXP), gExperienceTables[gSpeciesInfo[SPECIES_WYNAUT].growthRate][40]); + else + EXPECT_EQ(GetMonData(&gPlayerParty[1], MON_DATA_EXP), gExperienceTables[gSpeciesInfo[SPECIES_WYNAUT].growthRate][40]); + } +} + +#endif // I_EXP_SHARE_ITEM diff --git a/test/test_runner_battle.c b/test/test_runner_battle.c index a0a8033e154c..2d7d14833e1c 100644 --- a/test/test_runner_battle.c +++ b/test/test_runner_battle.c @@ -95,6 +95,7 @@ static void InvokeTestFunction(const struct BattleTest *test) switch (test->type) { case BATTLE_TEST_SINGLES: + case BATTLE_TEST_WILD: InvokeSingleTestFunctionWithStack(STATE->results, STATE->runParameter, &gBattleMons[B_POSITION_PLAYER_LEFT], &gBattleMons[B_POSITION_OPPONENT_LEFT], test->function.singles, &DATA.stack[BATTLE_TEST_STACK_SIZE]); break; case BATTLE_TEST_DOUBLES: @@ -236,7 +237,10 @@ static void BattleTest_Run(void *data) DATA.recordedBattle.opponentA = TRAINER_LINK_OPPONENT; DATA.recordedBattle.textSpeed = OPTIONS_TEXT_SPEED_FAST; - DATA.recordedBattle.battleFlags = BATTLE_TYPE_RECORDED_IS_MASTER | BATTLE_TYPE_RECORDED_LINK | BATTLE_TYPE_TRAINER | BATTLE_TYPE_IS_MASTER; + if (test->type == BATTLE_TEST_WILD) + DATA.recordedBattle.battleFlags = BATTLE_TYPE_IS_MASTER; + else + DATA.recordedBattle.battleFlags = BATTLE_TYPE_RECORDED_IS_MASTER | BATTLE_TYPE_RECORDED_LINK | BATTLE_TYPE_TRAINER | BATTLE_TYPE_IS_MASTER; if (test->type == BATTLE_TEST_DOUBLES) { DATA.recordedBattle.battleFlags |= BATTLE_TYPE_DOUBLE; @@ -293,7 +297,6 @@ static void BattleTest_Run(void *data) } SetVariablesForRecordedBattle(&DATA.recordedBattle); - if (STATE->trials) gMain.savedCallback = CB2_BattleTest_NextTrial; else if (STATE->parameters) @@ -708,6 +711,96 @@ void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP) } } +static s32 TryExp(s32 i, s32 n, u32 battlerId, u32 oldExp, u32 newExp) +{ + struct QueuedExpEvent *event; + s32 iMax = i + n; + for (; i < iMax; i++) + { + if (DATA.queuedEvents[i].type != QUEUED_EXP_EVENT) + continue; + + event = &DATA.queuedEvents[i].as.exp; + + if (event->battlerId == battlerId) + { + if (event->address <= 0xFFFF) + { + switch (event->type) + { + case EXP_EVENT_NEW_EXP: + if (event->address == newExp) + return i; + break; + case EXP_EVENT_DELTA_EXP: + if (event->address == 0) + return i; + else if ((s16)event->address == oldExp - newExp) + return i; + break; + } + } + else + { + switch (event->type) + { + case EXP_EVENT_NEW_EXP: + *(u32 *)event->address = newExp; + break; + case EXP_EVENT_DELTA_EXP: + *(s32 *)event->address = oldExp - newExp; + break; + } + return i; + } + } + } + return -1; +} + +void TestRunner_Battle_RecordExp(u32 battlerId, u32 oldExp, u32 newExp) +{ + s32 queuedEvent; + s32 match; + struct QueuedEvent *event; + + if (DATA.queuedEvent == DATA.queuedEventsCount) + return; + + event = &DATA.queuedEvents[DATA.queuedEvent]; + switch (event->groupType) + { + case QUEUE_GROUP_NONE: + case QUEUE_GROUP_ONE_OF: + if (TryExp(DATA.queuedEvent, event->groupSize, battlerId, oldExp, newExp) != -1) + DATA.queuedEvent += event->groupSize; + break; + case QUEUE_GROUP_NONE_OF: + queuedEvent = DATA.queuedEvent; + do + { + if ((match = TryExp(queuedEvent, event->groupSize, battlerId, oldExp, newExp)) != -1) + { + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.queuedEvents[match].sourceLineOffset); + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Matched EXPERIENCE_BAR", filename, line); + } + + queuedEvent += event->groupSize; + if (queuedEvent == DATA.queuedEventsCount) + break; + + event = &DATA.queuedEvents[queuedEvent]; + if (event->groupType == QUEUE_GROUP_NONE_OF) + continue; + + if (TryExp(queuedEvent, event->groupSize, battlerId, oldExp, newExp) != -1) + DATA.queuedEvent = queuedEvent + event->groupSize; + } while (FALSE); + break; + } +} + static s32 TryMessage(s32 i, s32 n, const u8 *string) { s32 j, k; @@ -866,6 +959,7 @@ static const char *const sEventTypeMacros[] = [QUEUED_ABILITY_POPUP_EVENT] = "ABILITY_POPUP", [QUEUED_ANIMATION_EVENT] = "ANIMATION", [QUEUED_HP_EVENT] = "HP_BAR", + [QUEUED_EXP_EVENT] = "EXPERIENCE_BAR", [QUEUED_MESSAGE_EVENT] = "MESSAGE", [QUEUED_STATUS_EVENT] = "STATUS_ICON", }; @@ -1276,6 +1370,12 @@ void Status1_(u32 sourceLine, u32 status1) SetMonData(DATA.currentMon, MON_DATA_STATUS, &status1); } +void OTName_(u32 sourceLine, const u8 *otName) +{ + INVALID_IF(!DATA.currentMon, "Traded outside of PLAYER/OPPONENT"); + SetMonData(DATA.currentMon, MON_DATA_OT_NAME, &otName); +} + static const char *const sBattlerIdentifiersSingles[] = { "player", @@ -1784,6 +1884,48 @@ void QueueHP(u32 sourceLine, struct BattlePokemon *battler, struct HPEventContex }; } +void QueueExp(u32 sourceLine, struct BattlePokemon *battler, struct ExpEventContext ctx) +{ + s32 battlerId = battler - gBattleMons; + u32 type; + uintptr_t address; + + INVALID_IF(!STATE->runScene, "EXPERIENCE_BAR outside of SCENE"); + if (DATA.queuedEventsCount == MAX_QUEUED_EVENTS) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: EXPERIENCE_BAR exceeds MAX_QUEUED_EVENTS", gTestRunnerState.test->filename, sourceLine); + + if (ctx.explicitExp) + { + type = EXP_EVENT_NEW_EXP; + address = (u32)ctx.exp; + } + else if (ctx.explicitCaptureGainedExp) + { + INVALID_IF(ctx.captureGainedExp == NULL, "captureGainedExp is NULL"); + type = EXP_EVENT_DELTA_EXP; + *ctx.captureGainedExp = 0; + address = (uintptr_t)ctx.captureGainedExp; + } + else + { + type = EXP_EVENT_DELTA_EXP; + address = 0; + } + + DATA.queuedEvents[DATA.queuedEventsCount++] = (struct QueuedEvent) { + .type = QUEUED_EXP_EVENT, + .sourceLineOffset = SourceLineOffset(sourceLine), + .groupType = QUEUE_GROUP_NONE, + .groupSize = 1, + .as = { .exp = { + .battlerId = battlerId, + .type = type, + .address = address, + }}, + }; +} + + void QueueMessage(u32 sourceLine, const u8 *pattern) { INVALID_IF(!STATE->runScene, "MESSAGE outside of SCENE");