Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleaned up Trainer Slides, added automated Trainer Slide tests, add new Enemy Critical Hit Slide #6018

Merged
merged 58 commits into from
Jan 23, 2025
Merged
Changes from 1 commit
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
daa2282
Moved trainer slides to their own file
pkmnsnfrn Jan 6, 2025
713e180
Reindent file
pkmnsnfrn Jan 6, 2025
5e98ad0
First version of slides without for loop is working
pkmnsnfrn Jan 6, 2025
60e34e3
Seperated frontier trainer into their own struct
pkmnsnfrn Jan 6, 2025
1584760
Reordered functions in trainer slide file
pkmnsnfrn Jan 7, 2025
9636df5
Changed names to make it clear that the condition is for the player t…
pkmnsnfrn Jan 7, 2025
307f5f7
Reorder functions in switch to match enum
pkmnsnfrn Jan 7, 2025
b2fe29d
Renamed PlayerMonUnaffected to EnemyMonUnaffected
pkmnsnfrn Jan 7, 2025
5eba4f9
Renamed PlayerMonUnaffected to EnemyMonUnaffected
pkmnsnfrn Jan 7, 2025
d4e32c1
Added ShouldInitializeFirstSTABMoveTrainerSlide
pkmnsnfrn Jan 8, 2025
674e70e
Fixed bug with ShouldRunTrainerSlideLastHalfHP
pkmnsnfrn Jan 8, 2025
63a620c
Renamed ShouldInitalizeFirstSTABMoveTrainerSlide to TryInitalizeFirst…
pkmnsnfrn Jan 8, 2025
dcc0ce5
Removed trainerSlide struct and replced with u8 array
pkmnsnfrn Jan 8, 2025
f3c2862
Introduced IsTrainerSlideInitialized
pkmnsnfrn Jan 8, 2025
038da37
Updated before first turn
pkmnsnfrn Jan 8, 2025
1c3f182
Rolled out use of DoesTrainerHaveSlideMessage
pkmnsnfrn Jan 8, 2025
6e193ee
Removed all old members of struct
pkmnsnfrn Jan 9, 2025
219da5d
Added TrainerSlideTargets enum and changed type of ShouldDoTrainerSlide
pkmnsnfrn Jan 9, 2025
579358a
enforced types on slideId and renamed variable
pkmnsnfrn Jan 9, 2025
01ef315
Moved Played and HaveMessage checks up one level
pkmnsnfrn Jan 9, 2025
ba75cb1
Created IsSlideinitalizedOrPlayed
pkmnsnfrn Jan 9, 2025
ac9fbc6
Removed retValue from header
pkmnsnfrn Jan 9, 2025
d184a6f
Fixed function headers
pkmnsnfrn Jan 9, 2025
52b3ff8
Replaced HP check with IsBattlerAlive
pkmnsnfrn Jan 10, 2025
697d9b5
Added checks to SanitizeTrainerId so tests don't fail
pkmnsnfrn Jan 10, 2025
2b68196
Added sTestTrainerSlides
pkmnsnfrn Jan 11, 2025
eadb65a
Moved sTest into its own file
pkmnsnfrn Jan 11, 2025
2b765f6
Add first critical hit test
pkmnsnfrn Jan 11, 2025
950338f
Updated spacing
pkmnsnfrn Jan 11, 2025
5f88b63
Added template for remaining work
pkmnsnfrn Jan 11, 2025
b044a50
Merged in master
pkmnsnfrn Jan 12, 2025
87fcb7c
Added gimmick fix for tests
pkmnsnfrn Jan 12, 2025
ea592f5
Made OpponentHandleTrainerSlide non-static and added to it recorded b…
pkmnsnfrn Jan 12, 2025
2a24052
Updated last low hp test
pkmnsnfrn Jan 12, 2025
7753d00
Updated last half hp test
pkmnsnfrn Jan 12, 2025
2ad7f67
Updated Mega slide test
pkmnsnfrn Jan 12, 2025
32848b8
Updated Z MOve Trainer Slide test
pkmnsnfrn Jan 12, 2025
829a7e0
Updated most tests
pkmnsnfrn Jan 12, 2025
3516c98
Removed function table and replaced with switch
pkmnsnfrn Jan 12, 2025
bab614c
Moved if statements out of functions per https://discord.com/channels…
pkmnsnfrn Jan 12, 2025
f11e018
Restored files
pkmnsnfrn Jan 12, 2025
f996725
Cleaned up some defines
pkmnsnfrn Jan 12, 2025
78f16ab
Removed testing code
pkmnsnfrn Jan 12, 2025
15a2f1a
Added new trainer slide: Enemy Mon Lands First Critical Hit
pkmnsnfrn Jan 12, 2025
c0222e4
Fixed Mega test to not rely on names
pkmnsnfrn Jan 12, 2025
bc8b9a9
Corrected spacing
pkmnsnfrn Jan 13, 2025
905d471
Revert "Added new trainer slide: Enemy Mon Lands First Critical Hit"
pkmnsnfrn Jan 13, 2025
d642468
Added new trainer slide: Enemy Mon Lands First Critical Hit
pkmnsnfrn Jan 13, 2025
12a0731
Added static assert to prevent users from adding more than 32 Trainer…
pkmnsnfrn Jan 13, 2025
5c34c20
Updated tutorial
pkmnsnfrn Jan 13, 2025
b38fec7
Updated SUMMARY.md
pkmnsnfrn Jan 13, 2025
10bf553
reset trainers.h
pkmnsnfrn Jan 13, 2025
7340fac
Changed 1u to 1 per https://github.com/rh-hideout/pokeemerald-expansi…
pkmnsnfrn Jan 13, 2025
c535ae4
Made trainer slide status arrays dynamic
pkmnsnfrn Jan 15, 2025
9fc8184
Updated documentation to match dynamic resizing
pkmnsnfrn Jan 15, 2025
994b3e6
Updated u32 to u8 on the struct to save some bits
pkmnsnfrn Jan 15, 2025
629f66f
Added named arugments per https://github.com/rh-hideout/pokeemerald-e…
pkmnsnfrn Jan 20, 2025
1c8459d
Apply suggestions from code review
pkmnsnfrn Jan 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Added static assert to prevent users from adding more than 32 Trainer…
… Slides
pkmnsnfrn committed Jan 13, 2025
commit 12a0731646ed2d3a412dcb167dd567a611ae8dc8
220 changes: 220 additions & 0 deletions docs/tutorials/how_to_new_trainer_slide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# 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](<https://github.com/rh-hideout/pokeemerald-expansion/commit/d6424688007cbd923c861cfd35272e5ebfaa4016>)
* [Patch](<https://github.com/rh-hideout/pokeemerald-expansion/commit/d6424688007cbd923c861cfd35272e5ebfaa4016.patch>)
* [Diff](<https://github.com/rh-hideout/pokeemerald-expansion/commit/d6424688007cbd923c861cfd35272e5ebfaa4016.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. Expansion, by default, will support 32 unique messages. If more than 32 are added without expanding the size of `slideMessageStatus`, the game will not compile.

#### Expanding `slideMessageStatus`

##### `include/trainer_slide.h`
```diff
struct MessageStatus
{
u32 messageInitalized : TRAINER_SLIDE_BITS;
u32 messageInitalized2 : TRAINER_SLIDE_BITS_2;
u32 messagePlayed : TRAINER_SLIDE_BITS;
u32 messagePlayed2 : TRAINER_SLIDE_BITS_2;
};
```

TODO

### `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`.
2 changes: 2 additions & 0 deletions include/constants/trainer_slide.h
Original file line number Diff line number Diff line change
@@ -29,6 +29,8 @@ enum TrainerSlideType
TRAINER_SLIDE_COUNT,
};

#define TRAINER_SLIDE_BITS min((TRAINER_SLIDE_COUNT - 1),32)

enum TrainerSlideTargets
{
TRAINER_SLIDE_TARGET_NONE,
4 changes: 2 additions & 2 deletions include/trainer_slide.h
Original file line number Diff line number Diff line change
@@ -5,8 +5,8 @@

struct MessageStatus
{
u32 messageInitalized;
u32 messagePlayed;
u32 messageInitalized : TRAINER_SLIDE_BITS;
u32 messagePlayed : TRAINER_SLIDE_BITS;
};

enum TrainerSlideTargets ShouldDoTrainerSlide(u32, enum TrainerSlideType);
2 changes: 2 additions & 0 deletions src/trainer_slide.c
Original file line number Diff line number Diff line change
@@ -37,6 +37,8 @@
#include "trainer_slide.h"
#include "battle_message.h"

STATIC_ASSERT(sizeof(gBattleStruct->slideMessageStatus) <= (2 * TRAINER_SLIDE_BITS), Too_Many_Trainer_Slides_Add_More_Members);

static u32 BattlerHPPercentage(u32, u32, u32);
static u32 GetEnemyMonCount(u32, u32, bool32);
static bool32 DoesTrainerHaveSlideMessage(enum DifficultyLevel, u32, u32);