diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index fe13ea72ee78..c6a61f776243 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -18,6 +18,7 @@ - [v1.7.x](tutorials/how_to_new_pokemon_1_7_0.md) - [v1.6.x](tutorials/how_to_new_pokemon_1_6_0.md) - [How to use the Testing System](tutorials/how_to_testing_system.md) + - [How to add new Trainer Slides](tutorials/how_to_new_trainer_slide.md) - [Changelog](./CHANGELOG.md) - [1.10.x]() - [Version 1.10.1](changelogs/1.10.x/1.10.1.md) diff --git a/docs/tutorials/how_to_new_trainer_slide.md b/docs/tutorials/how_to_new_trainer_slide.md new file mode 100644 index 000000000000..f8fa345ba6e9 --- /dev/null +++ b/docs/tutorials/how_to_new_trainer_slide.md @@ -0,0 +1,205 @@ +# Adding New Trainer Slides +## Define Slides Per Trainer + +We are going to add a Trainer Slide to Wally's first Victory Road battle, before he Mega Evolves his Gallade. This battle takes place outside a Battle Facility, so `sTrainerSlides` must be edited. + +### `src/trainer_slide.c` +```diff ++ const u8 gText_ThatsTheWay[] = _("That's the way, Gallade! Go!{PAUSE_UNTIL_PRESS}"); + +static const u8* const sTrainerSlides[DIFFICULTY_COUNT][TRAINERS_COUNT][TRAINER_SLIDE_COUNT] = +{ + [DIFFICULTY_NORMAL] = + { ++ [TRAINER_WALLY_VR_1] = // use the Trainer's Id from include/constants/opponents.h ++ { ++ [TRAINER_SLIDE_MEGA_EVOLUTION] = COMPOUND_STRING("That's the way, Gallade! Go!{PAUSE_UNTIL_PRESS}"), // find the id for the slide to be used. ++ //[TRAINER_SLIDE_MEGA_EVOLUTION] = gText_ThatsTheWay, // You can use globals or COMPOUND_STRING to define text here. ++ } + }, +}; +``` + +If we were to edit a Trainer that appears in a Battle Facility, `sFrontierTrainerSlides` would be edited instead. Here, we'll give Anabel a line before she uses a Z-Move. + +### `src/trainer_slide.c` +```diff +static const u8* const sFrontierTrainerSlides[DIFFICULTY_COUNT][FRONTIER_TRAINERS_COUNT][TRAINER_SLIDE_COUNT] = +{ + [DIFFICULTY_NORMAL] = + { ++ [TRAINER_ANABEL] = ++ { ++ [TRAINER_SLIDE_Z_MOVE] = COMPOUND_STRING("Victory...is ours!"), //{PAUSE_UNTIL_PRESS} is omitted, so the battle will continue as soon as the next is finished printing. ++ } + }, +}; +``` + +## Add New Slides +* [Example Commit]() +* [Patch]() +* [Diff]() + +### `include/constants/trainer_slide.h` +```diff +enum TrainerSlideType +{ + TRAINER_SLIDE_BEFORE_FIRST_TURN, + TRAINER_SLIDE_PLAYER_LANDS_FIRST_CRITICAL_HIT, ++ TRAINER_SLIDE_ENEMY_LANDS_FIRST_CRITICAL_HIT, // Each Trainer Slide has a unqiue id. + TRAINER_SLIDE_PLAYER_LANDS_FIRST_SUPER_EFFECTIVE_HIT, + TRAINER_SLIDE_PLAYER_LANDS_FIRST_STAB_MOVE, + TRAINER_SLIDE_PLAYER_LANDS_FIRST_DOWN, + TRAINER_SLIDE_ENEMY_MON_UNAFFECTED, + TRAINER_SLIDE_LAST_SWITCHIN, + TRAINER_SLIDE_LAST_HALF_HP, + TRAINER_SLIDE_LAST_LOW_HP, + TRAINER_SLIDE_MEGA_EVOLUTION, + TRAINER_SLIDE_Z_MOVE, + TRAINER_SLIDE_DYNAMAX, + TRAINER_SLIDE_COUNT, +}; +``` + +Each Trainer Slide has a unique id and needs to be added to this list. + +### `include/trainer_slide.h` + +If your new Trainer Slide needs to check for beforen initalized, a function is declared here to be used externally. Critical hits are used to initalize this Trainer Slide but the slide doesn't play instantly, so we will create an function to initialize it. + +```diff +void SetTrainerSlideMessage(enum DifficultyLevel, u32, u32); +void TryInitalizeFirstSTABMoveTrainerSlide(u32, u32, u32); +void TryInitalizeTrainerSlidePlayerLandsFirstCriticalHit(u32); ++ void TryInitalizeTrainerSlideEnemyLandsFirstCriticalHit(u32); +void TryInitalizeTrainerSlidePlayerLandsFirstSuperEffectiveHit(u32); +void TryInitalizeTrainerSlideEnemyMonUnaffected(u32); +bool32 IsTrainerSlideInitialized(enum TrainerSlideType); +``` +### `src/trainer_slide.c` + +```diff + return IsTrainerSlideInitialized(slideId); + } + ++static bool32 ShouldRunTrainerSlideEnemyLandsFirstCriticalHit(enum TrainerSlideType slideId) ++{ ++ return IsTrainerSlideInitialized(slideId); ++} ++ + static bool32 ShouldRunTrainerSlidePlayerLandsFirstSuperEffectiveHit(u32 battler, enum TrainerSlideType slideId) + { + if (!IsTrainerSlideInitialized(slideId)) +``` + +```diff + case TRAINER_SLIDE_PLAYER_LANDS_FIRST_CRITICAL_HIT: + shouldRun = ShouldRunTrainerSlidePlayerLandsFirstCriticalHit(slideId); + break; ++ case TRAINER_SLIDE_ENEMY_LANDS_FIRST_CRITICAL_HIT: ++ shouldRun = ShouldRunTrainerSlideEnemyLandsFirstCriticalHit(slideId); ++ break; + case TRAINER_SLIDE_PLAYER_LANDS_FIRST_SUPER_EFFECTIVE_HIT: + shouldRun = ShouldRunTrainerSlidePlayerLandsFirstSuperEffectiveHit(battler, slideId); + break; +``` + +The function that determines if a Slide should play has different function for most Slides. We need to check if this Slide was initialized by a critical hit previously, so a function is created here. This function and the Id and then added to the switch statement in `ShouldDoTrainerSlide`. + +```diff + InitalizeTrainerSlide(slideId); + } + ++void TryInitalizeTrainerSlideEnemyLandsFirstCriticalHit(u32 target) ++{ ++ enum TrainerSlideType slideId = TRAINER_SLIDE_ENEMY_LANDS_FIRST_CRITICAL_HIT; ++ ++ if (IsSlideInitalizedOrPlayed(slideId)) ++ return; ++ ++ if (GetBattlerSide(target) == B_SIDE_OPPONENT) ++ return; ++ ++ InitalizeTrainerSlide(slideId); ++} ++ +``` + +The function to check if this slide SHOULD be initalized is added to the bottom of this file. + +### `src/battle_main.c` + +```diff + BattleScriptExecute(i == 1 ? BattleScript_TrainerASlideMsgEnd2 : BattleScript_TrainerBSlideMsgEnd2); + else if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_PLAYER_LANDS_FIRST_CRITICAL_HIT))) + BattleScriptExecute(i == 1 ? BattleScript_TrainerASlideMsgEnd2 : BattleScript_TrainerBSlideMsgEnd2); ++ else if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_ENEMY_LANDS_FIRST_CRITICAL_HIT))) ++ BattleScriptExecute(i == 1 ? BattleScript_TrainerASlideMsgEnd2 : BattleScript_TrainerBSlideMsgEnd2); + else if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_PLAYER_LANDS_FIRST_SUPER_EFFECTIVE_HIT))) + BattleScriptExecute(i == 1 ? BattleScript_TrainerASlideMsgEnd2 : BattleScript_TrainerBSlideMsgEnd2); + else if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_PLAYER_LANDS_FIRST_STAB_MOVE))) +diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c +``` + +In `BattleTurnPassed`, most Trainer Slides are checked to see if they should run, so our new call is added here. + +### `src/battle_script_commands.c` + +```diff + { + PrepareStringBattle(STRINGID_CRITICALHIT, gBattlerAttacker); + ++ TryInitalizeTrainerSlideEnemyLandsFirstCriticalHit(gBattlerTarget); + TryInitalizeTrainerSlidePlayerLandsFirstCriticalHit(gBattlerTarget); + + gBattleCommunication[MSG_DISPLAY] = 1; +``` + +The actual usage of `TryInitalizeTrainerSlideEnemyLandsFirstCriticalHit` is added and is checked whenever a critical hit is scored. + +### `test/battle/trainer_slides.c` +```diff + } + } + ++SINGLE_BATTLE_TEST("Trainer Slide: Enemy Lands First Critical Hit") ++{ ++ gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_ENEMY_LANDS_FIRST_CRITICAL_HIT; ++ ++ GIVEN { ++ ASSUME(GetMoveEffect(MOVE_LASER_FOCUS) == EFFECT_LASER_FOCUS); ++ PLAYER(SPECIES_WOBBUFFET); ++ OPPONENT(SPECIES_WOBBUFFET); ++ } WHEN { ++ TURN { MOVE(opponent, MOVE_LASER_FOCUS); } ++ TURN { MOVE(opponent, MOVE_TACKLE); } ++ } SCENE { ++ ANIMATION(ANIM_TYPE_MOVE, MOVE_LASER_FOCUS, opponent); ++ ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); ++ MESSAGE("A critical hit!"); ++ MESSAGE("This message plays after the enemy lands their first critical hit.{PAUSE_UNTIL_PRESS}"); ++ } ++} ++ + SINGLE_BATTLE_TEST("Trainer Slide: Player Lands First STAB Hit") + { + gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_PLAYER_LANDS_FIRST_STAB_MOVE; +diff --git a/test/battle/trainer_slides.h b/test/battle/trainer_slides.h +``` + +### `test/battle/trainer_slides.h` +```diff + + [TRAINER_SLIDE_PLAYER_LANDS_FIRST_CRITICAL_HIT] = COMPOUND_STRING("This message plays after the player lands their first critical hit.{PAUSE_UNTIL_PRESS}"), + }, ++ [TRAINER_SLIDE_ENEMY_LANDS_FIRST_CRITICAL_HIT] = ++ { ++ [TRAINER_SLIDE_ENEMY_LANDS_FIRST_CRITICAL_HIT] = COMPOUND_STRING("This message plays after the enemy lands their first critical hit.{PAUSE_UNTIL_PRESS}"), ++ }, + [TRAINER_SLIDE_PLAYER_LANDS_FIRST_SUPER_EFFECTIVE_HIT] = + { + [TRAINER_SLIDE_PLAYER_LANDS_FIRST_SUPER_EFFECTIVE_HIT] = COMPOUND_STRING("This message plays after the player lands their first super effective hit.{PAUSE_UNTIL_PRESS}"), +``` + +Tests are added to make sure the new Trainer Slide works. A test is added to the c file, and the trainer to run the entry in the test is added to `sTestTrainerSlides`. diff --git a/include/battle.h b/include/battle.h index d6b5abb7f9c5..9ff6a16ec5cc 100644 --- a/include/battle.h +++ b/include/battle.h @@ -19,6 +19,7 @@ #include "battle_gimmick.h" #include "move.h" #include "random.h" // for rng_value_t +#include "trainer_slide.h" // Helper for accessing command arguments and advancing gBattlescriptCurrInstr. // @@ -790,16 +791,6 @@ struct BattleStruct u8 bonusCritStages[MAX_BATTLERS_COUNT]; // G-Max Chi Strike boosts crit stages of allies. u8 itemPartyIndex[MAX_BATTLERS_COUNT]; u8 itemMoveIndex[MAX_BATTLERS_COUNT]; - u8 trainerSlideFirstCriticalHitMsgState:2; - u8 trainerSlideFirstSuperEffectiveHitMsgState:2; - u8 trainerSlideFirstSTABMoveMsgState:2; - u8 trainerSlidePlayerMonUnaffectedMsgState:2; - u8 trainerSlideHalfHpMsgDone:1; - u8 trainerSlideMegaEvolutionMsgDone:1; - u8 trainerSlideZMoveMsgDone:1; - u8 trainerSlideBeforeFirstTurnMsgDone:1; - u8 trainerSlideDynamaxMsgDone:1; - u8 trainerSlideLowHpMsgDone:1; u8 pledgeMove:1; u8 isSkyBattle:1; u32 aiDelayTimer; // Counts number of frames AI takes to choose an action. @@ -838,6 +829,7 @@ struct BattleStruct u8 printedStrongWindsWeakenedAttack:1; u8 numSpreadTargets:2; u8 padding3:2; + struct MessageStatus slideMessageStatus; }; // The palaceFlags member of struct BattleStruct contains 1 flag per move to indicate which moves the AI should consider, diff --git a/include/battle_controllers.h b/include/battle_controllers.h index 5e92cac763ff..80d1b16f135f 100644 --- a/include/battle_controllers.h +++ b/include/battle_controllers.h @@ -323,6 +323,7 @@ void SetControllerToRecordedPlayer(u32 battler); // opponent controller void SetControllerToOpponent(u32 battler); +void OpponentHandleTrainerSlide(u32 battler); // player partner controller void Controller_PlayerPartnerShowIntroHealthbox(u32 battler); // Also used by the link partner. diff --git a/include/battle_message.h b/include/battle_message.h index 914e97d5d6b4..7acf90b4041e 100644 --- a/include/battle_message.h +++ b/include/battle_message.h @@ -245,29 +245,12 @@ struct BattleMsgData u8 textBuffs[3][TEXT_BUFF_ARRAY_COUNT]; }; -enum -{ - TRAINER_SLIDE_LAST_SWITCHIN, - TRAINER_SLIDE_LAST_LOW_HP, - TRAINER_SLIDE_FIRST_DOWN, - TRAINER_SLIDE_LAST_HALF_HP, - TRAINER_SLIDE_FIRST_CRITICAL_HIT, - TRAINER_SLIDE_FIRST_SUPER_EFFECTIVE_HIT, - TRAINER_SLIDE_FIRST_STAB_MOVE, - TRAINER_SLIDE_PLAYER_MON_UNAFFECTED, - TRAINER_SLIDE_MEGA_EVOLUTION, - TRAINER_SLIDE_Z_MOVE, - TRAINER_SLIDE_BEFORE_FIRST_TURN, - TRAINER_SLIDE_DYNAMAX, -}; - void BufferStringBattle(u16 stringID, u32 battler); u32 BattleStringExpandPlaceholdersToDisplayedString(const u8 *src); u32 BattleStringExpandPlaceholders(const u8 *src, u8 *dst, u32 dstSize); void BattlePutTextOnWindow(const u8 *text, u8 windowId); void SetPpNumbersPaletteInMoveSelection(u32 battler); u8 GetCurrentPpToMaxPpState(u8 currentPp, u8 maxPp); -u32 ShouldDoTrainerSlide(u32 battler, u32 which); // return 1 for TrainerA, 2 forTrainerB void ExpandBattleTextBuffPlaceholders(const u8 *src, u8 *dst); extern struct BattleMsgData *gBattleMsgDataPtr; diff --git a/include/constants/trainer_slide.h b/include/constants/trainer_slide.h new file mode 100644 index 000000000000..5ddab7be00d2 --- /dev/null +++ b/include/constants/trainer_slide.h @@ -0,0 +1,42 @@ +#ifndef GUARD_CONSTANTS_TRAINER_SLIDE_H +#define GUARD_CONSTANTS_TRAINER_SLIDE_H + +enum ComparisonOperators +{ + LESS_THAN, + EQUAL, + GREATER_THAN, + LESS_THAN_OR_EQUAL, + GREATER_THAN_OR_EQUAL, + NOT_EQUAL, +}; + +enum TrainerSlideType +{ + TRAINER_SLIDE_BEFORE_FIRST_TURN, + TRAINER_SLIDE_PLAYER_LANDS_FIRST_CRITICAL_HIT, + TRAINER_SLIDE_ENEMY_LANDS_FIRST_CRITICAL_HIT, + TRAINER_SLIDE_PLAYER_LANDS_FIRST_SUPER_EFFECTIVE_HIT, + TRAINER_SLIDE_PLAYER_LANDS_FIRST_STAB_MOVE, + TRAINER_SLIDE_PLAYER_LANDS_FIRST_DOWN, + TRAINER_SLIDE_ENEMY_MON_UNAFFECTED, + TRAINER_SLIDE_LAST_SWITCHIN, + TRAINER_SLIDE_LAST_HALF_HP, + TRAINER_SLIDE_LAST_LOW_HP, + TRAINER_SLIDE_MEGA_EVOLUTION, + TRAINER_SLIDE_Z_MOVE, + TRAINER_SLIDE_DYNAMAX, + TRAINER_SLIDE_COUNT, +}; + +#define TRAINER_SLIDES_PER_ARRAY 8 +#define TRAINER_SLIDE_ARRAY_SIZE ((TRAINER_SLIDE_COUNT + TRAINER_SLIDES_PER_ARRAY - 1) / TRAINER_SLIDES_PER_ARRAY) + +enum TrainerSlideTargets +{ + TRAINER_SLIDE_TARGET_NONE, + TRAINER_SLIDE_TARGET_TRAINER_A, + TRAINER_SLIDE_TARGET_TRAINER_B, +}; + +#endif // GUARD_CONSTANTS_TRAINER_SLIDE_H diff --git a/include/trainer_slide.h b/include/trainer_slide.h new file mode 100644 index 000000000000..c6f7b745e819 --- /dev/null +++ b/include/trainer_slide.h @@ -0,0 +1,25 @@ +#ifndef GUARD_TRAINER_SLIDE_H +#define GUARD_TRAINER_SLIDE_H + +#include "constants/trainer_slide.h" + + +struct MessageStatus +{ + u8 messageInitalized[TRAINER_SLIDE_ARRAY_SIZE]; + u8 messagePlayed[TRAINER_SLIDE_ARRAY_SIZE]; +}; + +void SetTrainerSlideMessage(enum DifficultyLevel difficulty, u32 trainerId, u32 slideId); +enum TrainerSlideTargets ShouldDoTrainerSlide(u32 battler, enum TrainerSlideType slideId); +void TryInitalizeFirstSTABMoveTrainerSlide(u32 battlerDef, u32 battlerAtk, u32 moveType); +void TryInitalizeTrainerSlidePlayerLandsFirstCriticalHit(u32 target); +void TryInitalizeTrainerSlideEnemyLandsFirstCriticalHit(u32 target); +void TryInitalizeTrainerSlidePlayerLandsFirstSuperEffectiveHit(u32 target); +void TryInitalizeTrainerSlideEnemyMonUnaffected(u32 target); +bool32 IsTrainerSlideInitialized(enum TrainerSlideType slideId); +bool32 IsTrainerSlidePlayed(enum TrainerSlideType slideId); +void InitalizeTrainerSlide(enum TrainerSlideType slideId); +void MarkTrainerSlideAsPlayed(enum TrainerSlideType slideId); + +#endif // GUARD_TRAINER_SLIDE_H diff --git a/src/battle_controller_opponent.c b/src/battle_controller_opponent.c index 898a59c2ee7f..f4b29ac23886 100644 --- a/src/battle_controller_opponent.c +++ b/src/battle_controller_opponent.c @@ -43,7 +43,6 @@ static void OpponentHandleLoadMonSprite(u32 battler); static void OpponentHandleSwitchInAnim(u32 battler); static void OpponentHandleDrawTrainerPic(u32 battler); -static void OpponentHandleTrainerSlide(u32 battler); static void OpponentHandleTrainerSlideBack(u32 battler); static void OpponentHandleMoveAnimation(u32 battler); static void OpponentHandlePrintString(u32 battler); @@ -508,7 +507,7 @@ static void OpponentHandleDrawTrainerPic(u32 battler) BtlController_HandleDrawTrainerPic(battler, trainerPicId, TRUE, xPos, 40, -1); } -static void OpponentHandleTrainerSlide(u32 battler) +void OpponentHandleTrainerSlide(u32 battler) { u32 trainerPicId = OpponentGetTrainerPicId(battler); BtlController_HandleTrainerSlide(battler, trainerPicId); diff --git a/src/battle_controller_recorded_opponent.c b/src/battle_controller_recorded_opponent.c index afd56a9faf42..085c760d7890 100644 --- a/src/battle_controller_recorded_opponent.c +++ b/src/battle_controller_recorded_opponent.c @@ -64,7 +64,7 @@ static void (*const sRecordedOpponentBufferCommands[CONTROLLER_CMDS_COUNT])(u32 [CONTROLLER_SWITCHINANIM] = RecordedOpponentHandleSwitchInAnim, [CONTROLLER_RETURNMONTOBALL] = BtlController_HandleReturnMonToBall, [CONTROLLER_DRAWTRAINERPIC] = RecordedOpponentHandleDrawTrainerPic, - [CONTROLLER_TRAINERSLIDE] = BtlController_Empty, + [CONTROLLER_TRAINERSLIDE] = OpponentHandleTrainerSlide, [CONTROLLER_TRAINERSLIDEBACK] = RecordedOpponentHandleTrainerSlideBack, [CONTROLLER_FAINTANIMATION] = BtlController_HandleFaintAnimation, [CONTROLLER_PALETTEFADE] = BtlController_Empty, diff --git a/src/battle_main.c b/src/battle_main.c index 5dba22a160c5..c1099b1711bb 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -70,6 +70,7 @@ #include "constants/party_menu.h" #include "constants/rgb.h" #include "constants/songs.h" +#include "constants/trainer_slide.h" #include "constants/trainers.h" #include "constants/weather.h" #include "cable_club.h" @@ -4076,13 +4077,15 @@ void BattleTurnPassed(void) BattleScriptExecute(i == 1 ? BattleScript_TrainerASlideMsgEnd2 : BattleScript_TrainerBSlideMsgEnd2); else if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_LAST_HALF_HP))) BattleScriptExecute(i == 1 ? BattleScript_TrainerASlideMsgEnd2 : BattleScript_TrainerBSlideMsgEnd2); - else if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_FIRST_CRITICAL_HIT))) + else if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_PLAYER_LANDS_FIRST_CRITICAL_HIT))) BattleScriptExecute(i == 1 ? BattleScript_TrainerASlideMsgEnd2 : BattleScript_TrainerBSlideMsgEnd2); - else if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_FIRST_SUPER_EFFECTIVE_HIT))) + else if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_ENEMY_LANDS_FIRST_CRITICAL_HIT))) BattleScriptExecute(i == 1 ? BattleScript_TrainerASlideMsgEnd2 : BattleScript_TrainerBSlideMsgEnd2); - else if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_FIRST_STAB_MOVE))) + else if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_PLAYER_LANDS_FIRST_SUPER_EFFECTIVE_HIT))) BattleScriptExecute(i == 1 ? BattleScript_TrainerASlideMsgEnd2 : BattleScript_TrainerBSlideMsgEnd2); - else if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_PLAYER_MON_UNAFFECTED))) + else if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_PLAYER_LANDS_FIRST_STAB_MOVE))) + BattleScriptExecute(i == 1 ? BattleScript_TrainerASlideMsgEnd2 : BattleScript_TrainerBSlideMsgEnd2); + else if ((i = ShouldDoTrainerSlide(GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT), TRAINER_SLIDE_ENEMY_MON_UNAFFECTED))) BattleScriptExecute(i == 1 ? BattleScript_TrainerASlideMsgEnd2 : BattleScript_TrainerBSlideMsgEnd2); } diff --git a/src/battle_message.c b/src/battle_message.c index a977bcd83c72..5ce953a65fe4 100644 --- a/src/battle_message.c +++ b/src/battle_message.c @@ -21,6 +21,7 @@ #include "test_runner.h" #include "text.h" #include "trainer_hill.h" +#include "trainer_slide.h" #include "window.h" #include "line_break.h" #include "constants/abilities.h" @@ -3541,252 +3542,3 @@ u8 GetCurrentPpToMaxPpState(u8 currentPp, u8 maxPp) return 0; } - -struct TrainerSlide -{ - u16 trainerId; - bool8 isFrontierTrainer; - const u8 *msgLastSwitchIn; - const u8 *msgLastLowHp; - const u8 *msgFirstDown; - const u8 *msgLastHalfHp; - const u8 *msgFirstCriticalHit; - const u8 *msgFirstSuperEffectiveHit; - const u8 *msgFirstSTABMove; - const u8 *msgPlayerMonUnaffected; - const u8 *msgMegaEvolution; - const u8 *msgZMove; - const u8 *msgBeforeFirstTurn; - const u8 *msgDynamax; -}; - -static const struct TrainerSlide sTrainerSlides[DIFFICULTY_COUNT][TRAINERS_COUNT] = -{ - [DIFFICULTY_NORMAL] = - { - /* Put any trainer slide-in messages inside this array. - Example: - { - .trainerId = TRAINER_WALLY_VR_2, - .isFrontierTrainer = FALSE, - .msgLastSwitchIn = sText_AarghAlmostHadIt, - .msgLastLowHp = sText_BoxIsFull, - .msgFirstDown = sText_123Poof, - .msgLastHalfHp = sText_ShootSoClose, - .msgFirstCriticalHit = sText_CriticalHit, - .msgFirstSuperEffectiveHit = sText_SuperEffective, - .msgFirstSTABMove = sText_ABoosted, - .msgPlayerMonUnaffected = sText_ButNoEffect, - .msgMegaEvolution = sText_PowderExplodes, - .msgZMove = sText_Electromagnetism, - .msgBeforeFirstTurn = sText_GravityIntensified, - .msgDynamax = sText_TargetWokeUp, - }, - }, - [DIFFICULTY_EASY] = - { - }, - [DIFFICULTY_HARD] = - { - */ - }, -}; - -static u32 GetEnemyMonCount(u32 firstId, u32 lastId, bool32 onlyAlive) -{ - u32 i, count = 0; - - for (i = firstId; i < lastId; i++) - { - u32 species = GetMonData(&gEnemyParty[i], MON_DATA_SPECIES_OR_EGG, NULL); - if (species != SPECIES_NONE - && species != SPECIES_EGG - && (!onlyAlive || GetMonData(&gEnemyParty[i], MON_DATA_HP, NULL))) - count++; - } - - return count; -} - -enum -{ - LESS_THAN, - EQUAL, - GREATER_THAN, - LESS_THAN_OR_EQUAL, - GREATER_THAN_OR_EQUAL, - NOT_EQUAL, -}; - -u32 BattlerHPPercentage(u32 battler, u32 operation, u32 threshold) -{ - switch (operation) - { - case LESS_THAN: - return gBattleMons[battler].hp < (gBattleMons[battler].maxHP / threshold); - case EQUAL: - return gBattleMons[battler].hp == (gBattleMons[battler].maxHP / threshold); - case GREATER_THAN: - return gBattleMons[battler].hp > (gBattleMons[battler].maxHP / threshold); - case LESS_THAN_OR_EQUAL: - return gBattleMons[battler].hp <= (gBattleMons[battler].maxHP / threshold); - case GREATER_THAN_OR_EQUAL: - return gBattleMons[battler].hp >= (gBattleMons[battler].maxHP / threshold); - case NOT_EQUAL: - default: - return gBattleMons[battler].hp != (gBattleMons[battler].maxHP / threshold); - } -} - -u32 ShouldDoTrainerSlide(u32 battler, u32 which) -{ - u32 i, firstId, lastId, trainerId, retValue = 1; - - if (!(gBattleTypeFlags & BATTLE_TYPE_TRAINER) || GetBattlerSide(battler) != B_SIDE_OPPONENT) - return 0; - - // Two opponents support. - if (gBattleTypeFlags & BATTLE_TYPE_TWO_OPPONENTS) - { - if (gBattlerPartyIndexes[battler] >= 3) - { - firstId = 3, lastId = PARTY_SIZE; - trainerId = gTrainerBattleOpponent_B; - retValue = 2; - } - else - { - firstId = 0, lastId = 3; - trainerId = gTrainerBattleOpponent_A; - } - } - else - { - firstId = 0, lastId = PARTY_SIZE; - trainerId = gTrainerBattleOpponent_A; - } - - enum DifficultyLevel difficulty = GetTrainerDifficultyLevel(trainerId); - - for (i = 0; i < ARRAY_COUNT(sTrainerSlides); i++) - { - if (trainerId == sTrainerSlides[difficulty]->trainerId - && (((gBattleTypeFlags & BATTLE_TYPE_FRONTIER) && sTrainerSlides[difficulty]->isFrontierTrainer) - || (!(gBattleTypeFlags & BATTLE_TYPE_FRONTIER) && !sTrainerSlides[difficulty]->isFrontierTrainer))) - { - gBattleScripting.battler = battler; - switch (which) - { - case TRAINER_SLIDE_LAST_SWITCHIN: - if (sTrainerSlides[difficulty]->msgLastSwitchIn != NULL && !CanBattlerSwitch(battler)) - { - gBattleStruct->trainerSlideMsg = sTrainerSlides[difficulty]->msgLastSwitchIn; - return retValue; - } - break; - case TRAINER_SLIDE_LAST_LOW_HP: - if (sTrainerSlides[difficulty]->msgLastLowHp != NULL - && GetEnemyMonCount(firstId, lastId, TRUE) == 1 - && BattlerHPPercentage(battler, LESS_THAN_OR_EQUAL, 4) - && !gBattleStruct->trainerSlideLowHpMsgDone) - { - gBattleStruct->trainerSlideLowHpMsgDone = TRUE; - gBattleStruct->trainerSlideMsg = sTrainerSlides[difficulty]->msgLastLowHp; - return retValue; - } - break; - case TRAINER_SLIDE_FIRST_DOWN: - if (sTrainerSlides[difficulty]->msgFirstDown != NULL && GetEnemyMonCount(firstId, lastId, TRUE) == GetEnemyMonCount(firstId, lastId, FALSE) - 1) - { - gBattleStruct->trainerSlideMsg = sTrainerSlides[difficulty]->msgFirstDown; - return retValue; - } - break; - case TRAINER_SLIDE_LAST_HALF_HP: - if (sTrainerSlides[difficulty]->msgLastHalfHp != NULL - && GetEnemyMonCount(firstId, lastId, TRUE) == GetEnemyMonCount(firstId, lastId, FALSE) - 1 - && BattlerHPPercentage(battler, LESS_THAN_OR_EQUAL, 2) && BattlerHPPercentage(battler, GREATER_THAN, 4) - && !gBattleStruct->trainerSlideHalfHpMsgDone) - { - gBattleStruct->trainerSlideHalfHpMsgDone = TRUE; - gBattleStruct->trainerSlideMsg = sTrainerSlides[difficulty]->msgLastHalfHp; - return TRUE; - } - break; - case TRAINER_SLIDE_FIRST_CRITICAL_HIT: - if (sTrainerSlides[difficulty]->msgFirstCriticalHit != NULL && gBattleStruct->trainerSlideFirstCriticalHitMsgState == 1) - { - gBattleStruct->trainerSlideFirstCriticalHitMsgState = 2; - gBattleStruct->trainerSlideMsg = sTrainerSlides[difficulty]->msgFirstCriticalHit; - return TRUE; - } - break; - case TRAINER_SLIDE_FIRST_SUPER_EFFECTIVE_HIT: - if (sTrainerSlides[difficulty]->msgFirstSuperEffectiveHit != NULL - && gBattleStruct->trainerSlideFirstSuperEffectiveHitMsgState == 1 - && gBattleMons[battler].hp) - { - gBattleStruct->trainerSlideFirstSuperEffectiveHitMsgState = 2; - gBattleStruct->trainerSlideMsg = sTrainerSlides[difficulty]->msgFirstSuperEffectiveHit; - return TRUE; - } - break; - case TRAINER_SLIDE_FIRST_STAB_MOVE: - if (sTrainerSlides[difficulty]->msgFirstSTABMove != NULL - && gBattleStruct->trainerSlideFirstSTABMoveMsgState == 1 - && GetEnemyMonCount(firstId, lastId, TRUE) == GetEnemyMonCount(firstId, lastId, FALSE)) - { - gBattleStruct->trainerSlideFirstSTABMoveMsgState = 2; - gBattleStruct->trainerSlideMsg = sTrainerSlides[difficulty]->msgFirstSTABMove; - return TRUE; - } - break; - case TRAINER_SLIDE_PLAYER_MON_UNAFFECTED: - if (sTrainerSlides[difficulty]->msgPlayerMonUnaffected != NULL - && gBattleStruct->trainerSlidePlayerMonUnaffectedMsgState == 1 - && GetEnemyMonCount(firstId, lastId, TRUE) == GetEnemyMonCount(firstId, lastId, FALSE)) - { - gBattleStruct->trainerSlidePlayerMonUnaffectedMsgState = 2; - gBattleStruct->trainerSlideMsg = sTrainerSlides[difficulty]->msgPlayerMonUnaffected; - return TRUE; - } - break; - case TRAINER_SLIDE_MEGA_EVOLUTION: - if (sTrainerSlides[difficulty]->msgMegaEvolution != NULL && !gBattleStruct->trainerSlideMegaEvolutionMsgDone) - { - gBattleStruct->trainerSlideMegaEvolutionMsgDone = TRUE; - gBattleStruct->trainerSlideMsg = sTrainerSlides[difficulty]->msgMegaEvolution; - return TRUE; - } - break; - case TRAINER_SLIDE_Z_MOVE: - if (sTrainerSlides[difficulty]->msgZMove != NULL && !gBattleStruct->trainerSlideZMoveMsgDone) - { - gBattleStruct->trainerSlideZMoveMsgDone = TRUE; - gBattleStruct->trainerSlideMsg = sTrainerSlides[difficulty]->msgZMove; - return TRUE; - } - break; - case TRAINER_SLIDE_BEFORE_FIRST_TURN: - if (sTrainerSlides[difficulty]->msgBeforeFirstTurn != NULL && !gBattleStruct->trainerSlideBeforeFirstTurnMsgDone) - { - gBattleStruct->trainerSlideBeforeFirstTurnMsgDone = TRUE; - gBattleStruct->trainerSlideMsg = sTrainerSlides[difficulty]->msgBeforeFirstTurn; - return TRUE; - } - break; - case TRAINER_SLIDE_DYNAMAX: - if (sTrainerSlides[difficulty]->msgDynamax != NULL && !gBattleStruct->trainerSlideDynamaxMsgDone) - { - gBattleStruct->trainerSlideDynamaxMsgDone = TRUE; - gBattleStruct->trainerSlideMsg = sTrainerSlides[difficulty]->msgDynamax; - return TRUE; - } - break; - } - break; - } - } - - return 0; -} diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index f5aef7a2c13f..1f39ac8877b4 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -61,6 +61,7 @@ #include "constants/party_menu.h" #include "constants/rgb.h" #include "constants/songs.h" +#include "constants/trainer_slide.h" #include "constants/trainers.h" #include "battle_util.h" #include "constants/pokemon.h" @@ -2702,9 +2703,8 @@ static void Cmd_critmessage(void) { PrepareStringBattle(STRINGID_CRITICALHIT, gBattlerAttacker); - // Signal for the trainer slide-in system. - if (GetBattlerSide(gBattlerTarget) != B_SIDE_PLAYER && gBattleStruct->trainerSlideFirstCriticalHitMsgState != 2) - gBattleStruct->trainerSlideFirstCriticalHitMsgState = 1; + TryInitalizeTrainerSlideEnemyLandsFirstCriticalHit(gBattlerTarget); + TryInitalizeTrainerSlidePlayerLandsFirstCriticalHit(gBattlerTarget); gBattleCommunication[MSG_DISPLAY] = 1; } @@ -2850,11 +2850,8 @@ static void Cmd_resultmessage(void) { stringId = STRINGID_SUPEREFFECTIVE; } - if (stringId) // Signal for the trainer slide-in system - { - if (GetBattlerSide(gBattlerTarget) != B_SIDE_PLAYER && gBattleStruct->trainerSlideFirstSuperEffectiveHitMsgState != 2) - gBattleStruct->trainerSlideFirstSuperEffectiveHitMsgState = 1; - } + if (stringId == STRINGID_SUPEREFFECTIVE || stringId == STRINGID_SUPEREFFECTIVETWOFOES) + TryInitalizeTrainerSlidePlayerLandsFirstSuperEffectiveHit(gBattlerTarget); break; case MOVE_RESULT_NOT_VERY_EFFECTIVE: if (IsDoubleSpreadMove()) @@ -10697,7 +10694,7 @@ static void Cmd_various(void) case VARIOUS_TRY_TRAINER_SLIDE_MSG_FIRST_OFF: { VARIOUS_ARGS(); - if ((i = ShouldDoTrainerSlide(battler, TRAINER_SLIDE_FIRST_DOWN))) + if ((i = ShouldDoTrainerSlide(battler, TRAINER_SLIDE_PLAYER_LANDS_FIRST_DOWN))) { gBattleScripting.battler = battler; BattleScriptPush(cmd->nextInstr); diff --git a/src/battle_util.c b/src/battle_util.c index 818c22a424b0..57b09afe9c11 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -24,6 +24,7 @@ #include "task.h" #include "test_runner.h" #include "trig.h" +#include "trainer_slide.h" #include "window.h" #include "battle_message.h" #include "battle_ai_main.h" @@ -1129,11 +1130,8 @@ void PrepareStringBattle(u16 stringId, u32 battler) SET_STATCHANGER(STAT_SPEED, 1, FALSE); } - // Signal for the trainer slide-in system. - if ((stringId == STRINGID_ITDOESNTAFFECT || stringId == STRINGID_PKMNWASNTAFFECTED || stringId == STRINGID_PKMNUNAFFECTED) - && GetBattlerSide(gBattlerTarget) == B_SIDE_OPPONENT - && gBattleStruct->trainerSlidePlayerMonUnaffectedMsgState != 2) - gBattleStruct->trainerSlidePlayerMonUnaffectedMsgState = 1; + if ((stringId == STRINGID_ITDOESNTAFFECT || stringId == STRINGID_PKMNWASNTAFFECTED || stringId == STRINGID_PKMNUNAFFECTED)) + TryInitalizeTrainerSlideEnemyMonUnaffected(gBattlerTarget); BtlController_EmitPrintString(battler, BUFFER_A, stringId); MarkBattlerForControllerExec(battler); @@ -10738,9 +10736,8 @@ static inline uq4_12_t CalcTypeEffectivenessMultiplierInternal(u32 move, u32 mov } } - // Signal for the trainer slide-in system. - if (GetBattlerSide(battlerDef) != B_SIDE_PLAYER && modifier && gBattleStruct->trainerSlideFirstSTABMoveMsgState != 2) - gBattleStruct->trainerSlideFirstSTABMoveMsgState = 1; + if (recordAbilities) + TryInitalizeFirstSTABMoveTrainerSlide(battlerDef, battlerAtk, moveType); return modifier; } diff --git a/src/trainer_slide.c b/src/trainer_slide.c new file mode 100644 index 000000000000..1606ae51558d --- /dev/null +++ b/src/trainer_slide.c @@ -0,0 +1,401 @@ +#include "global.h" +#include "battle.h" +#include "battle_anim.h" +#include "battle_controllers.h" +#include "battle_message.h" +#include "battle_setup.h" +#include "battle_tower.h" +#include "battle_z_move.h" +#include "data.h" +#include "event_data.h" +#include "frontier_util.h" +#include "graphics.h" +#include "international_string_util.h" +#include "item.h" +#include "link.h" +#include "menu.h" +#include "palette.h" +#include "recorded_battle.h" +#include "string_util.h" +#include "strings.h" +#include "test_runner.h" +#include "text.h" +#include "trainer_hill.h" +#include "window.h" +#include "line_break.h" +#include "constants/abilities.h" +#include "constants/battle_dome.h" +#include "constants/battle_string_ids.h" +#include "constants/frontier_util.h" +#include "constants/items.h" +#include "constants/moves.h" +#include "constants/opponents.h" +#include "constants/species.h" +#include "constants/trainers.h" +#include "constants/trainer_hill.h" +#include "constants/weather.h" +#include "trainer_slide.h" +#include "battle_message.h" + +static u32 BattlerHPPercentage(u32 battler, u32 operation, u32 threshold); +static u32 GetEnemyMonCount(u32 firstId, u32 lastId, bool32 onlyAlive); +static bool32 DoesTrainerHaveSlideMessage(enum DifficultyLevel difficulty, u32 trainerId, u32 slideId); +static bool32 ShouldRunTrainerSlidePlayerLandsFirstCriticalHit(enum TrainerSlideType slideId); +static bool32 ShouldRunTrainerSlideEnemyLandsFirstCriticalHit(enum TrainerSlideType slideId); +static bool32 ShouldRunTrainerSlidePlayerLandsFirstSuperEffectiveHit(u32 battler, enum TrainerSlideType slideId); +static bool32 ShouldRunTrainerSlidePlayerLandsFirstSTABMove(u32 firstId, u32 lastId, enum TrainerSlideType slideId); +static bool32 ShouldRunTrainerSlidePlayerLandsFirstDown(u32 firstId, u32 lastId); +static bool32 ShouldRunTrainerSlideEnemyMonUnaffected(u32 firstId, u32 lastId, enum TrainerSlideType slideId); +static bool32 ShouldRunTrainerSlideLastSwitchIn(u32 battler); +static bool32 ShouldRunTrainerSlideLastHalfHP(u32 firstId, u32 lastId, u32 battler); +static bool32 ShouldRunTrainerSlideLastLowHp(u32 firstId, u32 lastId, u32 battler); +static void SetTrainerSlideParamters(u32 battler, u32* firstId, u32* lastId, u32* trainerId, u32* retValue); +static bool32 IsSlideInitalizedOrPlayed(enum TrainerSlideType slideId); + +static const u8* const sTrainerSlides[DIFFICULTY_COUNT][TRAINERS_COUNT][TRAINER_SLIDE_COUNT] = +{ + [DIFFICULTY_NORMAL] = + { + }, +}; + +static const u8* const sFrontierTrainerSlides[DIFFICULTY_COUNT][TRAINERS_COUNT][TRAINER_SLIDE_COUNT] = +{ + [DIFFICULTY_NORMAL] = + { + }, +}; + +static const u8* const sTestTrainerSlides[DIFFICULTY_COUNT][FRONTIER_TRAINERS_COUNT][TRAINER_SLIDE_COUNT] = +{ +#include "../test/battle/trainer_slides.h" +}; + +static u32 BattlerHPPercentage(u32 battler, u32 operation, u32 threshold) +{ + switch (operation) + { + case LESS_THAN: + return gBattleMons[battler].hp < (gBattleMons[battler].maxHP / threshold); + case EQUAL: + return gBattleMons[battler].hp == (gBattleMons[battler].maxHP / threshold); + case GREATER_THAN: + return gBattleMons[battler].hp > (gBattleMons[battler].maxHP / threshold); + case LESS_THAN_OR_EQUAL: + return gBattleMons[battler].hp <= (gBattleMons[battler].maxHP / threshold); + case GREATER_THAN_OR_EQUAL: + return gBattleMons[battler].hp >= (gBattleMons[battler].maxHP / threshold); + case NOT_EQUAL: + default: + return gBattleMons[battler].hp != (gBattleMons[battler].maxHP / threshold); + } +} + +static u32 GetEnemyMonCount(u32 firstId, u32 lastId, bool32 onlyAlive) +{ + u32 i, count = 0; + + for (i = firstId; i < lastId; i++) + { + u32 species = GetMonData(&gEnemyParty[i], MON_DATA_SPECIES_OR_EGG, NULL); + if (species != SPECIES_NONE + && species != SPECIES_EGG + && (!onlyAlive || GetMonData(&gEnemyParty[i], MON_DATA_HP, NULL))) + count++; + } + + return count; +} + +static bool32 DoesTrainerHaveSlideMessage(enum DifficultyLevel difficulty, u32 trainerId, u32 slideId) +{ + if (gBattleTypeFlags & BATTLE_TYPE_FRONTIER) + return (sFrontierTrainerSlides[difficulty][trainerId][slideId] != NULL); + else if (TESTING) + return (sTestTrainerSlides[difficulty][trainerId][slideId] != NULL); + else + return (sTrainerSlides[difficulty][trainerId][slideId] != NULL); +} + +void SetTrainerSlideMessage(enum DifficultyLevel difficulty, u32 trainerId, u32 slideId) +{ + if (gBattleTypeFlags & BATTLE_TYPE_FRONTIER) + gBattleStruct->trainerSlideMsg = sFrontierTrainerSlides[difficulty][trainerId][slideId]; + else if (TESTING) + gBattleStruct->trainerSlideMsg = sTestTrainerSlides[difficulty][trainerId][slideId]; + else + gBattleStruct->trainerSlideMsg = sTrainerSlides[difficulty][trainerId][slideId]; +} + +static bool32 ShouldRunTrainerSlidePlayerLandsFirstCriticalHit(enum TrainerSlideType slideId) +{ + return IsTrainerSlideInitialized(slideId); +} + +static bool32 ShouldRunTrainerSlideEnemyLandsFirstCriticalHit(enum TrainerSlideType slideId) +{ + return IsTrainerSlideInitialized(slideId); +} + +static bool32 ShouldRunTrainerSlidePlayerLandsFirstSuperEffectiveHit(u32 battler, enum TrainerSlideType slideId) +{ + if (!IsTrainerSlideInitialized(slideId)) + return FALSE; + + if (!IsBattlerAlive(battler)) + return FALSE; + + return TRUE; +} + +static bool32 ShouldRunTrainerSlidePlayerLandsFirstSTABMove(u32 firstId, u32 lastId, enum TrainerSlideType slideId) +{ + if (!IsTrainerSlideInitialized(slideId)) + return FALSE; + + if (GetEnemyMonCount(firstId, lastId, TRUE) != GetEnemyMonCount(firstId, lastId, FALSE)) + return FALSE; + + return TRUE; +} + +static bool32 ShouldRunTrainerSlidePlayerLandsFirstDown(u32 firstId, u32 lastId) +{ + return ((GetEnemyMonCount(firstId, lastId, TRUE) == (GetEnemyMonCount(firstId, lastId, FALSE) - 1))); +} + +static bool32 ShouldRunTrainerSlideEnemyMonUnaffected(u32 firstId, u32 lastId, enum TrainerSlideType slideId) +{ + if (!IsTrainerSlideInitialized(slideId)) + return FALSE; + + return (GetEnemyMonCount(firstId, lastId, TRUE) == GetEnemyMonCount(firstId, lastId, FALSE)); +} + +static bool32 ShouldRunTrainerSlideLastSwitchIn(u32 battler) +{ + return !CanBattlerSwitch(battler); +} + +static bool32 ShouldRunTrainerSlideLastHalfHP(u32 firstId, u32 lastId, u32 battler) +{ + if (GetEnemyMonCount(firstId, lastId, TRUE) != 1) + return FALSE; + + if (BattlerHPPercentage(battler, GREATER_THAN, 2)) + return FALSE; + + return (BattlerHPPercentage(battler, GREATER_THAN, 4)); +} + +static bool32 ShouldRunTrainerSlideLastLowHp(u32 firstId, u32 lastId, u32 battler) +{ + if (GetEnemyMonCount(firstId, lastId, TRUE) != 1) + return FALSE; + + return (BattlerHPPercentage(battler, LESS_THAN_OR_EQUAL, 4)); +} + +static void SetTrainerSlideParamters(u32 battler, u32* firstId, u32* lastId, u32* trainerId, u32* retValue) +{ + if (gBattleTypeFlags & BATTLE_TYPE_TWO_OPPONENTS) + { + if (gBattlerPartyIndexes[battler] >= MULTI_PARTY_SIZE) + { + *firstId = MULTI_PARTY_SIZE; + *lastId = PARTY_SIZE; + *trainerId = SanitizeTrainerId(gTrainerBattleOpponent_B); + *retValue = TRAINER_SLIDE_TARGET_TRAINER_B; + } + else + { + *firstId = 0; + *lastId = MULTI_PARTY_SIZE; + *trainerId = SanitizeTrainerId(gTrainerBattleOpponent_A); + } + } + else + { + *firstId = 0; + *lastId = PARTY_SIZE; + *trainerId = SanitizeTrainerId(gTrainerBattleOpponent_A); + } +} + +enum TrainerSlideTargets ShouldDoTrainerSlide(u32 battler, enum TrainerSlideType slideId) +{ + u32 firstId, lastId, trainerId; + u32 retValue = TRAINER_SLIDE_TARGET_TRAINER_A; + bool32 shouldRun = FALSE; + + if (!(gBattleTypeFlags & BATTLE_TYPE_TRAINER) || GetBattlerSide(battler) != B_SIDE_OPPONENT) + return TRAINER_SLIDE_TARGET_NONE; + + SetTrainerSlideParamters(battler, &firstId, &lastId, &trainerId, &retValue); + enum DifficultyLevel difficulty = GetTrainerDifficultyLevel(trainerId); + + gBattleScripting.battler = battler; + + if (IsTrainerSlidePlayed(slideId)) + return TRAINER_SLIDE_TARGET_NONE; + + if (!DoesTrainerHaveSlideMessage(difficulty,trainerId,slideId)) + return TRAINER_SLIDE_TARGET_NONE; + + switch (slideId) + { + case TRAINER_SLIDE_PLAYER_LANDS_FIRST_CRITICAL_HIT: + shouldRun = ShouldRunTrainerSlidePlayerLandsFirstCriticalHit(slideId); + break; + case TRAINER_SLIDE_ENEMY_LANDS_FIRST_CRITICAL_HIT: + shouldRun = ShouldRunTrainerSlideEnemyLandsFirstCriticalHit(slideId); + break; + case TRAINER_SLIDE_PLAYER_LANDS_FIRST_SUPER_EFFECTIVE_HIT: + shouldRun = ShouldRunTrainerSlidePlayerLandsFirstSuperEffectiveHit(battler, slideId); + break; + case TRAINER_SLIDE_PLAYER_LANDS_FIRST_STAB_MOVE: + shouldRun = ShouldRunTrainerSlidePlayerLandsFirstSTABMove(firstId, lastId, slideId); + break; + case TRAINER_SLIDE_PLAYER_LANDS_FIRST_DOWN: + shouldRun = ShouldRunTrainerSlidePlayerLandsFirstDown(firstId, lastId); + break; + case TRAINER_SLIDE_ENEMY_MON_UNAFFECTED: + shouldRun = ShouldRunTrainerSlideEnemyMonUnaffected(firstId, lastId, slideId); + break; + case TRAINER_SLIDE_LAST_SWITCHIN: + shouldRun = ShouldRunTrainerSlideLastSwitchIn(battler); + break; + case TRAINER_SLIDE_LAST_HALF_HP: + shouldRun = ShouldRunTrainerSlideLastHalfHP(firstId, lastId, battler); + break; + case TRAINER_SLIDE_LAST_LOW_HP: + shouldRun = ShouldRunTrainerSlideLastLowHp(firstId, lastId, battler); + break; + case TRAINER_SLIDE_BEFORE_FIRST_TURN: + case TRAINER_SLIDE_MEGA_EVOLUTION: + case TRAINER_SLIDE_Z_MOVE: + case TRAINER_SLIDE_DYNAMAX: + shouldRun = TRUE; + break; + default: + return TRAINER_SLIDE_TARGET_NONE; + } + + if (shouldRun == FALSE) + return TRAINER_SLIDE_TARGET_NONE; + + MarkTrainerSlideAsPlayed(slideId); + SetTrainerSlideMessage(difficulty,trainerId,slideId); + return retValue; +} + +static bool32 IsSlideInitalizedOrPlayed(enum TrainerSlideType slideId) +{ + if (IsTrainerSlideInitialized(slideId)) + return TRUE; + + if (IsTrainerSlidePlayed(slideId)) + return TRUE; + + return FALSE; +} + +void TryInitalizeFirstSTABMoveTrainerSlide(u32 battlerDef, u32 battlerAtk, u32 moveType) +{ + enum TrainerSlideType slideId = TRAINER_SLIDE_PLAYER_LANDS_FIRST_STAB_MOVE; + + if (IsSlideInitalizedOrPlayed(slideId)) + return; + + if ((GetBattlerSide(battlerDef) == B_SIDE_PLAYER)) + return; + + if (IS_BATTLER_OF_TYPE(battlerAtk, moveType) == FALSE) + return; + + InitalizeTrainerSlide(slideId); +} + +void TryInitalizeTrainerSlidePlayerLandsFirstCriticalHit(u32 target) +{ + enum TrainerSlideType slideId = TRAINER_SLIDE_PLAYER_LANDS_FIRST_CRITICAL_HIT; + + if (IsSlideInitalizedOrPlayed(slideId)) + return; + + if (GetBattlerSide(target) == B_SIDE_PLAYER) + return; + + InitalizeTrainerSlide(slideId); +} + +void TryInitalizeTrainerSlideEnemyLandsFirstCriticalHit(u32 target) +{ + enum TrainerSlideType slideId = TRAINER_SLIDE_ENEMY_LANDS_FIRST_CRITICAL_HIT; + + if (IsSlideInitalizedOrPlayed(slideId)) + return; + + if (GetBattlerSide(target) == B_SIDE_OPPONENT) + return; + + InitalizeTrainerSlide(slideId); +} + +void TryInitalizeTrainerSlidePlayerLandsFirstSuperEffectiveHit(u32 target) +{ + enum TrainerSlideType slideId = TRAINER_SLIDE_PLAYER_LANDS_FIRST_SUPER_EFFECTIVE_HIT; + + if (IsSlideInitalizedOrPlayed(slideId)) + return; + + if (GetBattlerSide(target) == B_SIDE_PLAYER) + return; + + InitalizeTrainerSlide(slideId); +} + +void TryInitalizeTrainerSlideEnemyMonUnaffected(u32 target) +{ + enum TrainerSlideType slideId = TRAINER_SLIDE_ENEMY_MON_UNAFFECTED; + + if (IsSlideInitalizedOrPlayed(slideId)) + return; + + if (GetBattlerSide(target) != B_SIDE_OPPONENT) + return; + + InitalizeTrainerSlide(slideId); +} + +bool32 IsTrainerSlideInitialized(enum TrainerSlideType slideId) +{ + u32 arrayIndex = slideId / TRAINER_SLIDES_PER_ARRAY; + u32 bitPosition = slideId % TRAINER_SLIDES_PER_ARRAY; + + return (gBattleStruct->slideMessageStatus.messageInitalized[arrayIndex] & (1 << bitPosition)) != 0; +} + +bool32 IsTrainerSlidePlayed(enum TrainerSlideType slideId) +{ + u32 arrayIndex = slideId / TRAINER_SLIDES_PER_ARRAY; + u32 bitPosition = slideId % TRAINER_SLIDES_PER_ARRAY; + + return (gBattleStruct->slideMessageStatus.messagePlayed[arrayIndex] & (1 << bitPosition)) != 0; +} + +void InitalizeTrainerSlide(enum TrainerSlideType slideId) +{ + u32 arrayIndex = slideId / TRAINER_SLIDES_PER_ARRAY; + u32 bitPosition = slideId % TRAINER_SLIDES_PER_ARRAY; + + gBattleStruct->slideMessageStatus.messageInitalized[arrayIndex] |= (1 << bitPosition); +} + +void MarkTrainerSlideAsPlayed(enum TrainerSlideType slideId) +{ + u32 arrayIndex = slideId / TRAINER_SLIDES_PER_ARRAY; + u32 bitPosition = slideId % TRAINER_SLIDES_PER_ARRAY; + + gBattleStruct->slideMessageStatus.messagePlayed[arrayIndex] |= (1 << bitPosition); +} diff --git a/test/battle/trainer_slides.c b/test/battle/trainer_slides.c new file mode 100644 index 000000000000..b5bb127a0c7a --- /dev/null +++ b/test/battle/trainer_slides.c @@ -0,0 +1,214 @@ +#include "global.h" +#include "test/battle.h" +#include "battle_setup.h" + +SINGLE_BATTLE_TEST("Trainer Slide: Before First Turn") +{ + gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_BEFORE_FIRST_TURN; + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { } + } SCENE { + MESSAGE("This message plays before the first turn.{PAUSE_UNTIL_PRESS}"); + } +} + +SINGLE_BATTLE_TEST("Trainer Slide: Player Lands First Critical Hit") +{ + gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_PLAYER_LANDS_FIRST_CRITICAL_HIT; + + GIVEN { + ASSUME(GetMoveEffect(MOVE_LASER_FOCUS) == EFFECT_LASER_FOCUS); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_LASER_FOCUS); } + TURN { MOVE(player, MOVE_TACKLE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_LASER_FOCUS, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); + MESSAGE("A critical hit!"); + MESSAGE("This message plays after the player lands their first critical hit.{PAUSE_UNTIL_PRESS}"); + } +} + +SINGLE_BATTLE_TEST("Trainer Slide: Enemy Lands First Critical Hit") +{ + gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_ENEMY_LANDS_FIRST_CRITICAL_HIT; + + GIVEN { + ASSUME(GetMoveEffect(MOVE_LASER_FOCUS) == EFFECT_LASER_FOCUS); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_LASER_FOCUS); } + TURN { MOVE(opponent, MOVE_TACKLE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_LASER_FOCUS, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + MESSAGE("A critical hit!"); + MESSAGE("This message plays after the enemy lands their first critical hit.{PAUSE_UNTIL_PRESS}"); + } +} + +SINGLE_BATTLE_TEST("Trainer Slide: Player Lands First STAB Hit") +{ + gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_PLAYER_LANDS_FIRST_STAB_MOVE; + + GIVEN { + ASSUME((GetMoveType(MOVE_VINE_WHIP)) == gSpeciesInfo[SPECIES_BULBASAUR].types[0]); + PLAYER(SPECIES_BULBASAUR); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_VINE_WHIP); } + } SCENE { + MESSAGE("Bulbasaur used Vine Whip!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_VINE_WHIP, player); + MESSAGE("Player lands their first STAB move.{PAUSE_UNTIL_PRESS}"); + } +} + +SINGLE_BATTLE_TEST("Trainer Slide: Player Lands First Super Effective Hit") +{ + gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_PLAYER_LANDS_FIRST_SUPER_EFFECTIVE_HIT; + + GIVEN { + ASSUME(GetMoveType(MOVE_BITE) == TYPE_DARK); + ASSUME(gSpeciesInfo[SPECIES_WOBBUFFET].types[0] == TYPE_PSYCHIC); + ASSUME(gSpeciesInfo[SPECIES_WOBBUFFET].types[0] == TYPE_PSYCHIC); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_BITE); } + } SCENE { + MESSAGE("It's super effective!"); + MESSAGE("This message plays after the player lands their first super effective hit.{PAUSE_UNTIL_PRESS}"); + } +} + +SINGLE_BATTLE_TEST("Trainer Slide: Player Lands First Down") +{ + gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_PLAYER_LANDS_FIRST_DOWN; + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(opponent, MOVE_HEALING_WISH); SEND_OUT(opponent,1); } + } SCENE { + MESSAGE("The opposing Wobbuffet fainted!"); + MESSAGE("This message plays after the player KOs one enemy mon.{PAUSE_UNTIL_PRESS}"); + } +} + +SINGLE_BATTLE_TEST("Trainer Slide: Enemy Mon Unaffected") +{ + gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_ENEMY_MON_UNAFFECTED; + GIVEN { + ASSUME(B_SHEER_COLD_IMMUNITY >= GEN_7); + ASSUME(gSpeciesInfo[SPECIES_GLALIE].types[0] == TYPE_ICE); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_GLALIE); + } WHEN { + TURN { MOVE(player, MOVE_SHEER_COLD); } + } SCENE { + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_SHEER_COLD, player); + MESSAGE("It doesn't affect the opposing Glalieā€¦"); + MESSAGE("Player attacked enemy with ineffective move.{PAUSE_UNTIL_PRESS}"); + } +} + +SINGLE_BATTLE_TEST("Trainer Slide: Last Switchin") +{ + gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_LAST_SWITCHIN; + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(opponent, MOVE_HEALING_WISH); SEND_OUT(opponent,1); } + } SCENE { + MESSAGE("The opposing Wobbuffet fainted!"); + MESSAGE("This message plays after the enemy switches in their last Pokemon.{PAUSE_UNTIL_PRESS}"); + } +} + +SINGLE_BATTLE_TEST("Trainer Slide: Last Half Hp") +{ + gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_LAST_HALF_HP; + GIVEN { + ASSUME(gMovesInfo[MOVE_SUPER_FANG].effect == EFFECT_SUPER_FANG); + ASSUME(gSpeciesInfo[SPECIES_WOBBUFFET].baseHP == 190); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_SUPER_FANG); } + } SCENE { + MESSAGE("Enemy last Mon has < 51% HP.{PAUSE_UNTIL_PRESS}"); + } +} + +SINGLE_BATTLE_TEST("Trainer Slide: Last Low Hp") +{ + gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_LAST_LOW_HP; + GIVEN { + ASSUME(GetMoveEffect(MOVE_FALSE_SWIPE) == EFFECT_FALSE_SWIPE); + PLAYER(SPECIES_WOBBUFFET) { Attack(999);} + OPPONENT(SPECIES_WOBBUFFET) { Defense(1);} + } WHEN { + TURN { MOVE(player, MOVE_FALSE_SWIPE); } + } SCENE { + MESSAGE("Enemy last Mon has < 26% HP.{PAUSE_UNTIL_PRESS}"); + } +} + +SINGLE_BATTLE_TEST("Trainer Slide: Mega Evolution") +{ + gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_MEGA_EVOLUTION; + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_LOPUNNY) {Item(ITEM_LOPUNNITE); }; + } WHEN { + TURN { MOVE(opponent, MOVE_CELEBRATE, gimmick: GIMMICK_MEGA); } + } SCENE { + MESSAGE("This message plays before the enemy activates the Mega Evolution gimmick.{PAUSE_UNTIL_PRESS}"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_MEGA_EVOLUTION, opponent); + MESSAGE("The opposing Lopunny has Mega Evolved into Mega Lopunny!"); + } +} + +SINGLE_BATTLE_TEST("Trainer Slide: Z Move") +{ + gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_Z_MOVE; + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_NORMALIUM_Z); } + } WHEN { + TURN { MOVE(opponent, MOVE_QUICK_ATTACK, gimmick: GIMMICK_Z_MOVE); } + } SCENE { + MESSAGE("This message plays before the enemy activates the Z-Move gimmick.{PAUSE_UNTIL_PRESS}"); + MESSAGE("The opposing Wobbuffet surrounded itself with its Z-Power!"); + MESSAGE("The opposing Wobbuffet unleashes its full-force Z-Move!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_BREAKNECK_BLITZ, opponent); + } +} + +SINGLE_BATTLE_TEST("Trainer Slide: Dynamax") +{ + gBattleTestRunnerState->data.recordedBattle.opponentA = TRAINER_SLIDE_DYNAMAX; + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_CELEBRATE, gimmick: GIMMICK_DYNAMAX); } + } SCENE { + MESSAGE("This message plays before the enemy activates the Dynamax gimmick.{PAUSE_UNTIL_PRESS}"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_DYNAMAX_GROWTH, opponent); + } +} diff --git a/test/battle/trainer_slides.h b/test/battle/trainer_slides.h new file mode 100644 index 000000000000..cb62a0080037 --- /dev/null +++ b/test/battle/trainer_slides.h @@ -0,0 +1,56 @@ +[DIFFICULTY_NORMAL] = +{ + [TRAINER_SLIDE_BEFORE_FIRST_TURN] = + { + [TRAINER_SLIDE_BEFORE_FIRST_TURN] = COMPOUND_STRING("This message plays before the first turn.{PAUSE_UNTIL_PRESS}"), + }, + [TRAINER_SLIDE_PLAYER_LANDS_FIRST_CRITICAL_HIT] = + { + [TRAINER_SLIDE_PLAYER_LANDS_FIRST_CRITICAL_HIT] = COMPOUND_STRING("This message plays after the player lands their first critical hit.{PAUSE_UNTIL_PRESS}"), + }, + [TRAINER_SLIDE_ENEMY_LANDS_FIRST_CRITICAL_HIT] = + { + [TRAINER_SLIDE_ENEMY_LANDS_FIRST_CRITICAL_HIT] = COMPOUND_STRING("This message plays after the enemy lands their first critical hit.{PAUSE_UNTIL_PRESS}"), + }, + [TRAINER_SLIDE_PLAYER_LANDS_FIRST_SUPER_EFFECTIVE_HIT] = + { + [TRAINER_SLIDE_PLAYER_LANDS_FIRST_SUPER_EFFECTIVE_HIT] = COMPOUND_STRING("This message plays after the player lands their first super effective hit.{PAUSE_UNTIL_PRESS}"), + }, + [TRAINER_SLIDE_PLAYER_LANDS_FIRST_DOWN] = + { + [TRAINER_SLIDE_PLAYER_LANDS_FIRST_DOWN] = COMPOUND_STRING("This message plays after the player KOs one enemy mon.{PAUSE_UNTIL_PRESS}"), + }, + [TRAINER_SLIDE_ENEMY_MON_UNAFFECTED] = + { + [TRAINER_SLIDE_ENEMY_MON_UNAFFECTED] = COMPOUND_STRING("Player attacked enemy with ineffective move.{PAUSE_UNTIL_PRESS}"), + }, + [TRAINER_SLIDE_PLAYER_LANDS_FIRST_STAB_MOVE] = + { + [TRAINER_SLIDE_PLAYER_LANDS_FIRST_STAB_MOVE] = COMPOUND_STRING("Player lands their first STAB move.{PAUSE_UNTIL_PRESS}"), + }, + [TRAINER_SLIDE_LAST_SWITCHIN] = + { + [TRAINER_SLIDE_LAST_SWITCHIN] = COMPOUND_STRING("This message plays after the enemy switches in their last Pokemon.{PAUSE_UNTIL_PRESS}"), + }, + [TRAINER_SLIDE_LAST_HALF_HP] = + { + [TRAINER_SLIDE_LAST_HALF_HP] = COMPOUND_STRING("Enemy last Mon has < 51% HP.{PAUSE_UNTIL_PRESS}"), + }, + [TRAINER_SLIDE_LAST_LOW_HP] = + { + [TRAINER_SLIDE_LAST_LOW_HP] = COMPOUND_STRING("Enemy last Mon has < 26% HP.{PAUSE_UNTIL_PRESS}"), + }, + [TRAINER_SLIDE_MEGA_EVOLUTION] = + { + [TRAINER_SLIDE_MEGA_EVOLUTION] = COMPOUND_STRING("This message plays before the enemy activates the Mega Evolution gimmick.{PAUSE_UNTIL_PRESS}"), + }, + [TRAINER_SLIDE_Z_MOVE] = + { + [TRAINER_SLIDE_Z_MOVE] = COMPOUND_STRING("This message plays before the enemy activates the Z-Move gimmick.{PAUSE_UNTIL_PRESS}"), + }, + [TRAINER_SLIDE_DYNAMAX] = + { + [TRAINER_SLIDE_DYNAMAX] = COMPOUND_STRING("This message plays before the enemy activates the Dynamax gimmick.{PAUSE_UNTIL_PRESS}"), + }, +}, + diff --git a/test/test_runner_battle.c b/test/test_runner_battle.c index eda9c1eda552..2b6f16246bf3 100644 --- a/test/test_runner_battle.c +++ b/test/test_runner_battle.c @@ -2118,7 +2118,7 @@ void MoveGetIdAndSlot(s32 battlerId, struct MoveContext *ctx, u32 *moveId, u32 * u32 item = GetMonData(mon, MON_DATA_HELD_ITEM); u32 holdEffect = ItemId_GetHoldEffect(item); u32 species = GetMonData(mon, MON_DATA_SPECIES); - u32 side = GetBattlerSide(battlerId); + u32 side = battlerId & BIT_SIDE; // Check invalid item usage. INVALID_IF(ctx->gimmick == GIMMICK_MEGA && holdEffect != HOLD_EFFECT_MEGA_STONE && species != SPECIES_RAYQUAZA, "Cannot Mega Evolve without a Mega Stone");