Skip to content

Modify the enemy party loading system to allow nicknames, custom movesets, custom DVs and stat exp per mon (similar to Polished Crystal's system)

kagnusdev edited this page Jul 6, 2025 · 1 revision

Pokémon Red and Blue's method to change trainer movesets is rather limited so we're going to replace it with a system very similar to Pokémon Polished Crystal that will allow us to do more than just modifying the movesets.

Credits

This tutorial is heavily inspired by the pokecrystal tutorial on trainer paries and Pokémon Polished Crystal.

Note that I borrowed many macros from Polished Crystal and changed some names to help me better understand them.

Before starting

This is not a beginner level tutorial. This tutorial assumes you have a solid understanding of registers, flags, CPU instructions and directives, particularly macros. It will be hard to follow if you don't.

This tutorial also assumes that you're already using git (git is not GitHub).
Do not attempt to follow this tutorial if you're not using git. You want to be able to rollback changes easily.
You should build at the end of each part and commit if it builds and works as expected. The exception may be part 3 if you run out of space in the bank, just commit part 3 and 4 in one go.

If you're not already using git, you can start by following this free tutorial. No need to create an account, and you already installed it as part of the pokered installation so you can skip that part too.
Using git will help you way beyond this tutorial, so for your own good, don't skip this.

Contents

  1. Rewrite party data as macros
  2. Replace the terminator byte with a leading size byte
  3. Replace the level byte with flags and include a level byte for every mon
  4. Make the party data able to be in different banks and move it to its own section
  5. Include mon moves directly into party data and remove the old way of customizing moves
  6. Add support for custom DVs per mon
  7. Add support for custom stat exp per mon
  8. Add support for mon nicknames
  9. Add checks to avoid mistakes when using the macros
  10. Going further
    1. Improving the code that points to correct mon data
    2. Improving DVs and stat exp macros
      1. Improve tr_dvs and tr_stat_exp input
      2. Moving DVs and stat exp into spread tables
    3. Remove useless instructions
    4. Out of scope but related features
  11. Feature branch

1. Rewrite party data as macros

The pokered party data format is quite simple. The format we aiming to change it to is less simple. Changing dbs manually with each step is going to be very tedious if we don't use macros, so we'll start by replacing the existing dbs by macros that generate the same output. We'll then be able to modify the macros to change the output without any change to data/trainers/parties.asm needed.

First let's create a constants/trainer_data_constants.asm file:

+; legacy value that is not simple flags
+DEF TRAINERTYPE_MULTI_LEVELS EQU $FF

It's quite empty for now but we'll add more in future steps.

Next, let's borrow macros from Polished Crystal and adapt them to gen 1.
Create a data/trainers/macros.asm file:

+DEF _tr_class = 0
+
+; Usage: def_trainer_class <CLASS_CONSTANT>
+; CLASS_CONSTANT is defined in trainer_constants.asm
+MACRO def_trainer_class
+	assert \1 == _tr_class, "Trainer class ID mismatch"
+	def _tr_class += 1
+	def _tr_party = 1
+ENDM
+
+; Usage: def_trainer <TRAINER_INDEX>, <PARTY_LEVEL>
+; TRAINER_INDEX is 1-based
+; PARTY_LEVEL is the level for the whole party, use TRAINERTYPE_MULTI_LEVELS to set mon levels individually
+MACRO def_trainer
+	; Reset trainer macro state.
+	def _tr_flags = 0
+	def _tr_mons = 0
+	def _tr_nick_lengths = 0
+	assert \1 == _tr_party, "Trainer party ID mismatch"
+	def _tr_lv = \2
+	def _tr_size = 0
+	def _tr_party += 1
+ENDM
+
+; Usage: tr_mon [LEVEL,] <SPECIES>
+; LEVEL determines the level of the mon, it is required if trainer level was set to TRAINERTYPE_MULTI_LEVELS.
+; SPECIES is the species.
+MACRO tr_mon
+	; First, reset all stale data from the previous Trainer's mons.
+	def p = _tr_mons
+
+	if _tr_lv == TRAINERTYPE_MULTI_LEVELS
+		assert _NARG == 2, "Trainer party requires a level for each mon"
+		; Then actually define the data. Level is required for multi.
+		def _tr_pk{d:p}_level = \1
+		SHIFT
+	else
+		; defining the value anyway for easier refactoring
+		def _tr_pk{d:p}_level = _tr_lv
+	endc
+
+	redef _tr_pk{d:p}_species EQUS "\1"
+	def _tr_mons += 1
+ENDM
+
+; Write out the party data from stored trainer buffer.
+MACRO end_trainer
+	; First, write the byte length of the party.
+	; Pokémon data
+	if _tr_lv == TRAINERTYPE_MULTI_LEVELS
+		def _tr_size += 2 ; level, species
+	else
+		def _tr_size += 1 ; species
+	endc
+
+	def _tr_size *= _tr_mons
+
+	; Trainer level
+	def _tr_size += 1
+
+	; Party size should never exceed 255, but just in case...
+	if _tr_size > 255
+		fail "Party size too large"
+	endc
+
+	; level or magic value for multi level
+	db _tr_lv
+
+	; Now for all the mon data.
+	for p, _tr_mons
+		if _tr_lv == TRAINERTYPE_MULTI_LEVELS
+			db _tr_pk{d:p}_level, _tr_pk{d:p}_species
+		else
+			db _tr_pk{d:p}_species
+		endc
+	endr
+
+	db 0 ; terminator
+ENDM

These macros are meant to be used like the example below in the end.
Note that the commented out macros were not added yet.

NewRivalData:
	def_trainer_class NEW_RIVAL
	def_trainer 1, TRAINERTYPE_MULTI_LEVELS
	tr_mon 12, NIDORAN_F
		; tr_nick "Nido"
		; tr_moves GROWL, TACKLE, SCRATCH, DOUBLE_KICK
	tr_mon 10, JIGGLYPUFF
		; tr_nick "Jiggly"
		; tr_moves SING, POUND
	tr_mon 10, CLEFAIRY
		; tr_nick "Clefy"
		; tr_moves POUND, GROWL, METRONOME
	tr_mon 13, SQUIRTLE
		; tr_nick "Blasty"
		; tr_moves TACKLE, TAIL_WHIP, BUBBLE, WITHDRAW
		; tr_dvs $FF, $FF
		; tr_stat_exp 2560, 0, 0, 0, 0 ; 1 HP vitamin
	tr_mon 15, DITTO
		; tr_nick "Ditty"
		; tr_moves TRANSFORM
	end_trainer

Don't continue if you're still not using git. I repeat, don't continue if you're still not using git. If anything goes wrong with the parties file update, you'll be able to discard the changes easily.

Update your data/trainers/parties.asm to match this one. You can either directly download the file or, if you already modified the parties before, this ruby script should help you generate your custom file without erasing your changes.

If you want to use the script, you'll probably need to install ruby (in WSL if you're on Windows):

sudo apt-get update && sudo apt-get install ruby -y

To use it, first make sure it's executable:

chmod +x transform_parties_data.rb

Then you can run it to preview the changes (you need to replace path/to/parties.asm with the actual path):

transform_parties_data.rb path/to/parties.asm | less

If everything looks right to you, you can run it in rewrite mode:

transform_parties_data.rb --rewrite path/to/parties.asm

Now it should build and the ROM should still match the checksum you had before the changes (pokered's if you didn't change anything yet).

Be glad, you'll no longer get weird party problems because of forgetting a separator.

2. Replace the terminator byte with a leading size byte

Right now, 0 is a data separator, meaning we can't use it as an actual value, like 0 stat exp in attack for an Alakazam. Also, we have to read bytes 1 by 1 until we find a separator when finding the correct party. Replacing the terminator byte with a leading size byte has 2 advantages: we can use 0 in mon data and we can just add the size to hl to get the next party data.

Edit the end_trainer macro in data/trainers/macros.asm to change the format:

 		fail "Party size too large"
 	endc
 
+	; replace terminator byte with size byte
+	db _tr_size ; new way to skip trainers
 	; level or magic value for multi level
 	db _tr_lv
 			db _tr_pk{d:p}_species
 		endc
 	endr
-
-	db 0 ; terminator
 ENDM

Edit engine/battle/read_trainer_party.asm to handle the new format.

First part is the trainers skipping. Instead of looking for the terminator, we read the size and add it to hl until we find the right trainer. We're also renaming the label because there is no inner loop anymore:

 ; and hl points to the trainer class.
 ; Our next task is to iterate through the trainers,
 ; decrementing b each time, until we get to the right one.
-.outer
+.nextTrainer
 	dec b
 	jr z, .IterateTrainer
-.inner
 	ld a, [hli]
-	and a
-	jr nz, .inner
-	jr .outer
+	add l
+	ld l, a
+	adc h
+	sub l
+	ld h, a
+	jr .nextTrainer
 
 ; if the first byte of trainer data is FF,
 ; - each pokemon has a specific level

Now, we edit the party reading code by loading the size in c and decreasing it every time we read a byte from the party and moving the condition to the end of the loop:

 ; else the first byte is the level of every pokemon on the team
 .IterateTrainer
 	ld a, [hli]
+	ld c, a
+	ld a, [hli]
+	dec c
 	cp $FF ; is the trainer special?
 	jr z, .SpecialTrainer ; if so, check for special moves
 	ld [wCurEnemyLevel], a
 .LoopTrainerData
 	ld a, [hli]
-	and a ; have we reached the end of the trainer data?
-	jr z, .FinishUp
 	ld [wCurPartySpecies], a
 	ld a, ENEMY_PARTY_DATA
 	ld [wMonDataLocation], a
 	push hl
 	call AddPartyMon
 	pop hl
+	dec c ; have we reached the end of the trainer data?
+	jr z, .FinishUp
 	jr .LoopTrainerData
 .SpecialTrainer
 ; if this code is being run:

That was for the regular parties, now let's handle the special ones:

 ;      (as opposed to the whole team being of the same level)
 ; - if [wLoneAttackNo] != 0, one pokemon on the team has a special move
 	ld a, [hli]
-	and a ; have we reached the end of the trainer data?
-	jr z, .AddLoneMove
+	dec c
 	ld [wCurEnemyLevel], a
 	ld a, [hli]
 	ld [wCurPartySpecies], a

Let's fall through at the end of the loop instead of jumping from the beginning:

 	push hl
 	call AddPartyMon
 	pop hl
-	jr .SpecialTrainer
+	dec c ; have we reached the end of the trainer data?
+	jr nz, .SpecialTrainer
 .AddLoneMove
 ; does the trainer have a single monster with a different move?
 	ld a, [wLoneAttackNo] ; Brock is 01, Misty is 02, Erika is 04, etc

Notice that in both case, we don't decrement c right after reading the species but after adding the mon to the enemy party. This is to use the decrement as a stop condition.

3. Replace the level byte with flags and include a level byte for every mon

We won't add every new attributes to all parties, so we need a way to tell the game what's in the party. To do that, we'll replace the level byte with a flags byte.

Thanks to all the previous changes, we can now include 0s in party data, which is necessary since it's our flags' default value.

Let's update data/trainers/macros.asm b/data/trainers/macros.asm to make the level optional in def_trainer:

 	def _tr_party = 1
 ENDM
 
-; Usage: def_trainer <TRAINER_INDEX>, <PARTY_LEVEL>
+; Usage: def_trainer <TRAINER_INDEX>, [PARTY_LEVEL]
 ; TRAINER_INDEX is 1-based
-; PARTY_LEVEL is the level for the whole party, use TRAINERTYPE_MULTI_LEVELS to set mon levels individually
+; PARTY_LEVEL is the level for the whole party, defaults to TRAINERTYPE_MULTI_LEVELS to set mon levels individually
 MACRO def_trainer
 	; Reset trainer macro state.
 	def _tr_flags = 0
 	def _tr_mons = 0
 	def _tr_nick_lengths = 0
 	assert \1 == _tr_party, "Trainer party ID mismatch"
-	def _tr_lv = \2
+	if _NARG == 2
+		def _tr_lv = \2
+	else
+		def _tr_lv = TRAINERTYPE_MULTI_LEVELS
+	endc
 	def _tr_size = 0
 	def _tr_party += 1
 ENDM

Let's replace the level byte with a flags byte:

 MACRO end_trainer
 	; First, write the byte length of the party.
 	; Pokémon data
-	if _tr_lv == TRAINERTYPE_MULTI_LEVELS
-		def _tr_size += 2 ; level, species
-	else
-		def _tr_size += 1 ; species
-	endc
+	def _tr_size += 2 ; level, species
 
 	def _tr_size *= _tr_mons
 
-	; Trainer level
+	; Trainer flags
 	def _tr_size += 1
 
 	; Party size should never exceed 255, but just in case...

Let's always include the level before the species:

 	; replace terminator byte with size byte
 	db _tr_size ; new way to skip trainers
-	; level or magic value for multi level
-	db _tr_lv
+	; party flags
+	db _tr_flags
 
 	; Now for all the mon data.
 	for p, _tr_mons
-		if _tr_lv == TRAINERTYPE_MULTI_LEVELS
-			db _tr_pk{d:p}_level, _tr_pk{d:p}_species
-		else
-			db _tr_pk{d:p}_species
-		endc
+
+		db _tr_pk{d:p}_level, _tr_pk{d:p}_species
 	endr
 ENDM

Then let's move the constants inclusion to includes.asm:

 INCLUDE "constants/text_constants.asm"
 INCLUDE "constants/menu_constants.asm"
 
+INCLUDE "constants/trainer_data_constants.asm"
+
 IF DEF(_RED_VC)
 INCLUDE "vc/pokered.constants.asm"
 ENDC

removing it form data/trainers/parties.asm:

-INCLUDE "constants/trainer_data_constants.asm"
 INCLUDE "data/trainers/macros.asm"
 
 TrainerDataPointers:

Let's dedicate a space in ram/wram.asm to store the flags:

 wSerialEnemyDataBlock:: ; ds $1a8
 
-	ds 9
+	ds 8
 
+wEnemyPartyFlags:: db
 wEnemyPartyCount:: db
 wEnemyPartySpecies:: ds PARTY_LENGTH + 1

Now we can modify engine/battle/read_trainer_party.asm to handle to the new format. Store the flags in WRAM, delete the .SpecialTrainer handling and make it replace .LoopTrainerData:

 	ld c, a
 	ld a, [hli]
 	dec c
-	cp $FF ; is the trainer special?
-	jr z, .SpecialTrainer ; if so, check for special moves
-	ld [wCurEnemyLevel], a
+	ld [wEnemyPartyFlags], a
 .LoopTrainerData
-	ld a, [hli]
-	ld [wCurPartySpecies], a
-	ld a, ENEMY_PARTY_DATA
-	ld [wMonDataLocation], a
-	push hl
-	call AddPartyMon
-	pop hl
-	dec c ; have we reached the end of the trainer data?
-	jr z, .FinishUp
-	jr .LoopTrainerData
-.SpecialTrainer
-; if this code is being run:
-; - each pokemon has a specific level
-;      (as opposed to the whole team being of the same level)
 ; - if [wLoneAttackNo] != 0, one pokemon on the team has a special move
 	ld a, [hli]
 	dec c
call AddPartyMon
 	pop hl
 	dec c ; have we reached the end of the trainer data?
-	jr nz, .SpecialTrainer
+	jr nz, .LoopTrainerData
 .AddLoneMove
 ; does the trainer have a single monster with a different move?
 	ld a, [wLoneAttackNo] ; Brock is 01, Misty is 02, Erika is 04, etc

We're now ready to add flags but regular trainer party data grew a lot, so let's handle that first.

If your ROM doesn't build because of the bank growing too big, don't commit and fix the error by applying the next part.

4. Make the party data able to be in different banks and move it to its own section

We don't want to overflow the bank holding the party data, which has $41a bytes free in vanilla, but only $f0 after applying the previous changes to vanilla. You may have already overflown if you already modified the trainer parties prior to this tutorial. It's time to move parties to their own section.

Let's create data/trainers/parties_pointers.asm that will hold the bank prefixed pointers:

+TrainerDataPointers:
+	table_width 3
+	dba YoungsterData
+	dba BugCatcherData
+	dba LassData
+	dba SailorData
+	dba JrTrainerMData
+	dba JrTrainerFData
+	dba PokemaniacData
+	dba SuperNerdData
+	dba HikerData
+	dba BikerData
+	dba BurglarData
+	dba EngineerData
+	dba UnusedJugglerData
+	dba FisherData
+	dba SwimmerData
+	dba CueBallData
+	dba GamblerData
+	dba BeautyData
+	dba PsychicData
+	dba RockerData
+	dba JugglerData
+	dba TamerData
+	dba BirdKeeperData
+	dba BlackbeltData
+	dba Rival1Data
+	dba ProfOakData
+	dba ChiefData
+	dba ScientistData
+	dba GiovanniData
+	dba RocketData
+	dba CooltrainerMData
+	dba CooltrainerFData
+	dba BrunoData
+	dba BrockData
+	dba MistyData
+	dba LtSurgeData
+	dba ErikaData
+	dba KogaData
+	dba BlaineData
+	dba SabrinaData
+	dba GentlemanData
+	dba Rival2Data
+	dba Rival3Data
+	dba LoreleiData
+	dba ChannelerData
+	dba AgathaData
+	dba LanceData
+	assert_table_length NUM_TRAINERS

Let's change engine/battle/trainer_ai.asm to include that new file

 INCLUDE "data/trainers/special_moves.asm"
 
-INCLUDE "data/trainers/parties.asm"
+INCLUDE "data/trainers/parties_pointers.asm"
 
 TrainerAI:
 	and a

Let's remove TrainerDataPointers from data/trainers/parties.asm:

INCLUDE "data/trainers/macros.asm"
 
-TrainerDataPointers:
-	table_width 2
-	dw YoungsterData
-	dw BugCatcherData
-	dw LassData
-	dw SailorData
-	dw JrTrainerMData
-	dw JrTrainerFData
-	dw PokemaniacData
-	dw SuperNerdData
-	dw HikerData
-	dw BikerData
-	dw BurglarData
-	dw EngineerData
-	dw UnusedJugglerData
-	dw FisherData
-	dw SwimmerData
-	dw CueBallData
-	dw GamblerData
-	dw BeautyData
-	dw PsychicData
-	dw RockerData
-	dw JugglerData
-	dw TamerData
-	dw BirdKeeperData
-	dw BlackbeltData
-	dw Rival1Data
-	dw ProfOakData
-	dw ChiefData
-	dw ScientistData
-	dw GiovanniData
-	dw RocketData
-	dw CooltrainerMData
-	dw CooltrainerFData
-	dw BrunoData
-	dw BrockData
-	dw MistyData
-	dw LtSurgeData
-	dw ErikaData
-	dw KogaDatacall 
-	dw BlaineData
-	dw SabrinaData
-	dw GentlemanData
-	dw Rival2Data
-	dw Rival3Data
-	dw LoreleiData
-	dw ChannelerData
-	dw AgathaData
-	dw LanceData
-	assert_table_length NUM_TRAINERS
-

Let's include the actual parties in main.asm, in their own section:

INCLUDE "engine/battle/move_effects/reflect_light_screen.asm"
 
 
+SECTION "Trainer Parties 1", ROMX
+
+INCLUDE "data/trainers/parties.asm"
+
+
 SECTION "Battle Core", ROMX
 
 INCLUDE "engine/battle/core.asm"

Now let's borrow a routine from gen 2 and adapt it to gen 1. Add it to home/copy.asm:

	or b
 	jr nz, CopyData
 	ret
+
+; retrieve a single byte from a:hl, and return it in a.
+GetFarByte::
+	; bankswitch to new bank
+	call BankswitchHome
+
+	; get byte from new bank
+	ld a, [hl]
+	ldh [hFarByte], a
+
+	; bankswitch to previous bank
+	call BankswitchBack
+
+	; return retrieved value in a
+	ldh a, [hFarByte]
+	ret

Let's define a place to store that hFarByte in ram/hram.asm:

 	ds 1
 ENDU
 
-	ds 4
+	ds 3
+
+hFarByte:: db
 
 hWhoseTurn:: db ; 0 on player's turn, 1 on enemy's turn

We need to store the bank that holds the party data in ram/wram.asm:

 wSerialEnemyDataBlock:: ; ds $1a8

-	ds 8
+	ds 7

+wEnemyPartyBank:: db
wEnemyPartyFlags:: db
wEnemyPartyCount:: db

Now, we can change engine/battle/read_trainer_party.asm.

First let's create a helper routine borrowed from Polished Crystal:

+GetNextTrainerDataByte:
+	ld a, [wEnemyPartyBank]
+	call GetFarByte
+	inc hl
+	ret
+
 ReadTrainer:
 
 ; don't change any moves in a link battle

Then handle the new entry size for TrainerDataPointers:

; get the pointer to trainer data for this class
 	ld a, [wCurOpponent]
 	sub OPP_ID_OFFSET + 1 ; convert value from pokemon to trainer
+	ld c, a
 	add a
+	add c
 	ld hl, TrainerDataPointers
 	ld c, a
 	ld b, 0
 	add hl, bc ; hl points to trainer class
 	ld a, [hli]
+	ld [wEnemyPartyBank], a
+	ld a, [hli]
 	ld h, [hl]
 	ld l, a
 	ld a, [wTrainerNo]

Note that this work since there are less than 86 trainer classes (85 * 3 == 255) and that you're unlikely to reach that number (you'd need to apply the trainers are not pokémon tutorial first anyway), so I didn't bother to add an assert. Feel free to add one if you want to.

Then use our helper to fetch the party data:

.nextTrainer
 	dec b
 	jr z, .IterateTrainer
-	ld a, [hli]
+	call GetNextTrainerDataByte
 	add l
 	ld l, a
 	adc h
 .IterateTrainer
-	ld a, [hli]
+	call GetNextTrainerDataByte
 	ld c, a
-	ld a, [hli]
+	call GetNextTrainerDataByte
 	dec c
 	ld [wEnemyPartyFlags], a
 .LoopTrainerData
; - if [wLoneAttackNo] != 0, one pokemon on the team has a special move
-	ld a, [hli]
+	call GetNextTrainerDataByte
 	dec c
 	ld [wCurEnemyLevel], a
-	ld a, [hli]
+	call GetNextTrainerDataByte
 	ld [wCurPartySpecies], a
 	ld a, ENEMY_PARTY_DATA
 	ld [wMonDataLocation], a

Now we're finally ready to add flags to our parties and your ROM should build if it didn't in the previous part.

5. Include mon moves directly into party data and remove the old way of customizing moves

Let's start with the flag for moves.

Let's add it to constants/trainer_data_constants.asm:

+; Trainer party types (see engine/battle/read_trainer_party.asm)
+	const_def
+	shift_const TRAINERTYPE_MOVES     ; bit 0
 ; legacy value that is not simple flags
 DEF TRAINERTYPE_MULTI_LEVELS EQU $FF

Let's modify the macros.
Define default values for moves in tr_mon:

 	; First, reset all stale data from the previous Trainer's mons.
 	def p = _tr_mons
 
+	for i, 1, NUM_MOVES + 1
+		def _tr_pk{d:p}_move{d:i} = NO_MOVE
+	endr
+
 	if _tr_lv == TRAINERTYPE_MULTI_LEVELS
 		assert _NARG == 2, "Trainer party requires a level for each mon"
 		; Then actually define the data. Level is required for multi.

Add tr_moves macro:

 	def _tr_mons += 1
 ENDM
 
+; Usage: tr_moves <MOVE1>, [MOVE2], [MOVE3], [MOVE4]
+; MOVE* defines a mon's moves. You can specify between 1-4 moves.
+MACRO tr_moves
+	def _tr_flags |= TRAINERTYPE_MOVES
+	if _NARG > NUM_MOVES
+		fail "A mon may only have {d:NUM_MOVES} moves."
+	endc
+	for i, 1, _NARG + 1
+		def _tr_pk{d:p}_move{d:i} = \<i>
+	endr
+ENDM
+
 ; Write out the party data from stored trainer buffer.

Add moves size to party size in end_trainer:

MACRO end_trainer
	; First, write the byte length of the party.
	; Pokémon data
	def _tr_size += 2 ; level, species

+	if _tr_flags & TRAINERTYPE_MOVES
+		def _tr_size += NUM_MOVES
+	endc
+
	def _tr_size *= _tr_mons

	; Trainer flags

Add moves to party data in end_trainer:

	; Now for all the mon data.
	for p, _tr_mons
+		; We can't have implicit moves, for now.
+		if (_tr_flags & TRAINERTYPE_MOVES) && _tr_pk{d:p}_move1 == NO_MOVE
+			fail "Unspecified move list for _tr_pk{d:p}_species"
+		endc
 
 		db _tr_pk{d:p}_level, _tr_pk{d:p}_species
+
+		if _tr_flags & TRAINERTYPE_MOVES
+			for i, 1, NUM_MOVES + 1
+				db _tr_pk{d:p}_move{d:i}
+			endr
+		endc
 	endr
 ENDM

Edit the trainer parties to include custom moves on gym leaders, elite 4 and champion.
Giovanni:

; Viridian Gym
 	def_trainer 3, TRAINERTYPE_MULTI_LEVELS
 	tr_mon 45, RHYHORN
+		tr_moves STOMP, TAIL_WHIP, FURY_ATTACK, HORN_DRILL
 	tr_mon 42, DUGTRIO
+		tr_moves GROWL, DIG, SAND_ATTACK, SLASH
 	tr_mon 44, NIDOQUEEN
+		tr_moves SCRATCH, TAIL_WHIP, BODY_SLAM, POISON_STING
 	tr_mon 45, NIDOKING
+		tr_moves TACKLE, HORN_ATTACK, POISON_STING, THRASH
 	tr_mon 50, RHYDON
+		tr_moves STOMP, TAIL_WHIP, FISSURE, HORN_DRILL
 	end_trainer

Bruno and other gym leaders:

 	def_trainer_class BRUNO
 	def_trainer 1, TRAINERTYPE_MULTI_LEVELS
 	tr_mon 53, ONIX
+		tr_moves ROCK_THROW, RAGE, SLAM, HARDEN
 	tr_mon 55, HITMONCHAN
+		tr_moves ICE_PUNCH, THUNDERPUNCH, MEGA_PUNCH, COUNTER
 	tr_mon 55, HITMONLEE
+		tr_moves JUMP_KICK, FOCUS_ENERGY, HI_JUMP_KICK, MEGA_KICK
 	tr_mon 56, ONIX
+		tr_moves ROCK_THROW, RAGE, SLAM, HARDEN
 	tr_mon 58, MACHAMP
+		tr_moves LEER, FOCUS_ENERGY, FISSURE, SUBMISSION
 	end_trainer


 BrockData:
 	def_trainer_class BROCK
 	def_trainer 1, TRAINERTYPE_MULTI_LEVELS
 	tr_mon 12, GEODUDE
+		tr_moves TACKLE, DEFENSE_CURL
 	tr_mon 14, ONIX
+		tr_moves TACKLE, SCREECH, BIDE
 	end_trainer

 MistyData:
 	def_trainer_class MISTY
 	def_trainer 1, TRAINERTYPE_MULTI_LEVELS
 	tr_mon 18, STARYU
+		tr_moves TACKLE, WATER_GUN
 	tr_mon 21, STARMIE
+		tr_moves TACKLE, WATER_GUN, BUBBLEBEAM
 	end_trainer


 LtSurgeData:
 	def_trainer_class LT_SURGE
 	def_trainer 1, TRAINERTYPE_MULTI_LEVELS
 	tr_mon 21, VOLTORB
+		tr_moves TACKLE, SCREECH, SONICBOOM
 	tr_mon 18, PIKACHU
+		tr_moves THUNDERSHOCK, GROWL, THUNDER_WAVE, QUICK_ATTACK
 	tr_mon 24, RAICHU
+		tr_moves THUNDERSHOCK, GROWL, THUNDERBOLT
 	end_trainer
 
 
 ErikaData:
 	def_trainer_class ERIKA
 	def_trainer 1, TRAINERTYPE_MULTI_LEVELS
 	tr_mon 29, VICTREEBEL
+		tr_moves RAZOR_LEAF, WRAP, POISONPOWDER, SLEEP_POWDER
 	tr_mon 24, TANGELA
+		tr_moves CONSTRICT, BIND
 	tr_mon 29, VILEPLUME
+		tr_moves PETAL_DANCE, POISONPOWDER, MEGA_DRAIN, SLEEP_POWDER
 	end_trainer
 
 
 KogaData:
 	def_trainer_class KOGA
 	def_trainer 1, TRAINERTYPE_MULTI_LEVELS
 	tr_mon 37, KOFFING
+		tr_moves TACKLE, SMOG, SLUDGE, SMOKESCREEN
 	tr_mon 39, MUK
+		tr_moves DISABLE, POISON_GAS, MINIMIZE, SLUDGE
 	tr_mon 37, KOFFING
+		tr_moves TACKLE, SMOG, SLUDGE, SMOKESCREEN
 	tr_mon 43, WEEZING
+		tr_moves SMOG, SLUDGE, TOXIC, SELFDESTRUCT
 	end_trainer
 
 
 BlaineData:
 	def_trainer_class BLAINE
 	def_trainer 1, TRAINERTYPE_MULTI_LEVELS
 	tr_mon 42, GROWLITHE
+		tr_moves EMBER, LEER, TAKE_DOWN, AGILITY
 	tr_mon 40, PONYTA
+		tr_moves TAIL_WHIP, STOMP, GROWL, FIRE_SPIN
 	tr_mon 42, RAPIDASH
+		tr_moves TAIL_WHIP, STOMP, GROWL, FIRE_SPIN
 	tr_mon 47, ARCANINE
+		tr_moves ROAR, EMBER, FIRE_BLAST, TAKE_DOWN
 	end_trainer
 
 
 SabrinaData:
 	def_trainer_class SABRINA
 	def_trainer 1, TRAINERTYPE_MULTI_LEVELS
 	tr_mon 38, KADABRA
+		tr_moves DISABLE, PSYBEAM, RECOVER, PSYCHIC_M
 	tr_mon 37, MR_MIME
+		tr_moves CONFUSION, BARRIER, LIGHT_SCREEN, DOUBLESLAP
 	tr_mon 38, VENOMOTH
+		tr_moves POISONPOWDER, LEECH_LIFE, STUN_SPORE, PSYBEAM
 	tr_mon 43, ALAKAZAM
+		tr_moves PSYBEAM, RECOVER, PSYWAVE, REFLECT
 	end_trainer

Champion and Lorelei:

 Rival3Data:
 	def_trainer_class RIVAL3
 	def_trainer 1, TRAINERTYPE_MULTI_LEVELS
 	tr_mon 61, PIDGEOT
+		tr_moves WHIRLWIND, WING_ATTACK, SKY_ATTACK, MIRROR_MOVE
 	tr_mon 59, ALAKAZAM
+		tr_moves PSYBEAM, RECOVER, PSYCHIC_M, REFLECT
 	tr_mon 61, RHYDON
+		tr_moves TAIL_WHIP, FURY_ATTACK, HORN_DRILL, LEER
 	tr_mon 61, ARCANINE
+		tr_moves ROAR, EMBER, LEER, TAKE_DOWN
 	tr_mon 63, EXEGGUTOR
+		tr_moves BARRAGE, HYPNOSIS, STOMP
 	tr_mon 65, BLASTOISE
+		tr_moves BITE, WITHDRAW, BLIZZARD, HYDRO_PUMP
 	end_trainer
 
 	def_trainer 2, TRAINERTYPE_MULTI_LEVELS
 	tr_mon 61, PIDGEOT
+		tr_moves WHIRLWIND, WING_ATTACK, SKY_ATTACK, MIRROR_MOVE
 	tr_mon 59, ALAKAZAM
+		tr_moves PSYBEAM, RECOVER, PSYCHIC_M, REFLECT
 	tr_mon 61, RHYDON
+		tr_moves TAIL_WHIP, FURY_ATTACK, HORN_DRILL, LEER
 	tr_mon 61, GYARADOS
+		tr_moves DRAGON_RAGE, LEER, HYDRO_PUMP, HYPER_BEAM
 	tr_mon 63, ARCANINE
+		tr_moves ROAR, EMBER, LEER, TAKE_DOWN
 	tr_mon 65, VENUSAUR
+		tr_moves RAZOR_LEAF, GROWTH, MEGA_DRAIN, SOLARBEAM
 	end_trainer
 
 	def_trainer 3, TRAINERTYPE_MULTI_LEVELS
 	tr_mon 61, PIDGEOT
+		tr_moves WHIRLWIND, WING_ATTACK, SKY_ATTACK, MIRROR_MOVE
 	tr_mon 59, ALAKAZAM
+		tr_moves PSYBEAM, RECOVER, PSYCHIC_M, REFLECT
 	tr_mon 61, RHYDON
+		tr_moves TAIL_WHIP, FURY_ATTACK, HORN_DRILL, LEER
 	tr_mon 61, EXEGGUTOR
+		tr_moves BARRAGE, HYPNOSIS, STOMP
 	tr_mon 63, GYARADOS
+		tr_moves DRAGON_RAGE, LEER, HYDRO_PUMP, HYPER_BEAM
 	tr_mon 65, CHARIZARD
+		tr_moves RAGE, SLASH, FIRE_BLAST, FIRE_SPIN
 	end_trainer
 
 
 LoreleiData:
 	def_trainer_class LORELEI
 	def_trainer 1, TRAINERTYPE_MULTI_LEVELS
 	tr_mon 54, DEWGONG
+		tr_moves GROWL, AURORA_BEAM, REST, TAKE_DOWN
 	tr_mon 53, CLOYSTER
+		tr_moves SUPERSONIC, CLAMP, AURORA_BEAM, SPIKE_CANNON
 	tr_mon 54, SLOWBRO
+		tr_moves GROWL, WATER_GUN, WITHDRAW, AMNESIA
 	jr z, .LoopTrainerData
 
 	tr_mon 56, JYNX
+		tr_moves DOUBLESLAP, ICE_PUNCH, BODY_SLAM, THRASH
 	tr_mon 56, LAPRAS
+		tr_moves BODY_SLAM, CONFUSE_RAY, BLIZZARD, HYDRO_PUMP
 	end_trainer

Agatha and Lance:

AgathaData:
	def_trainer_class AGATHA
	def_trainer 1, TRAINERTYPE_MULTI_LEVELS
	tr_mon 56, GENGAR
+		tr_moves CONFUSE_RAY, NIGHT_SHADE, HYPNOSIS, DREAM_EATER
	tr_mon 56, GOLBAT
+		tr_moves SUPERSONIC, CONFUSE_RAY, WING_ATTACK, HAZE
	tr_mon 55, HAUNTER
+		tr_moves CONFUSE_RAY, NIGHT_SHADE, HYPNOSIS, DREAM_EATER
	jr z, .LoopTrainerData

	tr_mon 58, ARBOK
+		tr_moves BITE, GLARE, SCREECH, ACID
	tr_mon 60, GENGAR
+		tr_moves CONFUSE_RAY, NIGHT_SHADE, TOXIC, DREAM_EATER
	end_trainer


LanceData:
	def_trainer_class LANCE
	def_trainer 1, TRAINERTYPE_MULTI_LEVELS
	tr_mon 58, GYARADOS
+		tr_moves DRAGON_RAGE, LEER, HYDRO_PUMP, HYPER_BEAM
	tr_mon 56, DRAGONAIR
+		tr_moves AGILITY, SLAM, DRAGON_RAGE, HYPER_BEAM
	tr_mon 56, DRAGONAIR
+		tr_moves AGILITY, SLAM, DRAGON_RAGE, HYPER_BEAM
	tr_mon 60, AERODACTYL
+		tr_moves SUPERSONIC, BITE, TAKE_DOWN, HYPER_BEAM
	tr_mon 62, DRAGONITE
+		tr_moves AGILITY, SLAM, BARRIER, HYPER_BEAM
	end_trainer

Now, it's time to edit engine/battle/read_trainer_party.asm to load those moves.
Decrementing c on every read would become too painful so let's use another way, borrowed from Polished Crystal, to check the end of data:

 	call GetNextTrainerDataByte
 	dec c
 	ld [wEnemyPartyFlags], a
+	; c is remaining trainer data size
+	; so trainer data ends at hl + c
+	; set c to l + c to stop reading when l == c
+	ld a, l
+	add c
+	ld c, a
+	push bc
 .LoopTrainerData
-; - if [wLoneAttackNo] != 0, one pokemon on the team has a special move
+	pop bc
+	ld a, l
+	sub c ; have we reached the end of the trainer data?
+	jr z, .FinishUp
+	push bc
+
 	call GetNextTrainerDataByte
-	dec c
 	ld [wCurEnemyLevel], a
 	call GetNextTrainerDataByte
 	ld [wCurPartySpecies], a

As a bonus this allows to use bc in the loop body, which is exactly what we're going to do.

Let's also remove the old code that handled custom moves in vanilla:

 	push hl
 	call AddPartyMon
 	pop hl
-	dec c ; have we reached the end of the trainer data?
-	jr nz, .LoopTrainerData
-.AddLoneMove
-; does the trainer have a single monster with a different move?
-	ld a, [wLoneAttackNo] ; Brock is 01, Misty is 02, Erika is 04, etc
-	and a
-	jr z, .AddTeamMove
-	dec a
-	add a
-	ld c, a
-	ld b, 0
-	ld hl, LoneMoves
-	add hl, bc
-	ld a, [hli]
-	ld d, [hl]
-	ld hl, wEnemyMon1Moves + 2
-	ld bc, wEnemyMon2 - wEnemyMon1
-	call AddNTimes
-	ld [hl], d
-	jr .FinishUp
-.AddTeamMove
-; check if our trainer's team has special moves
 
-; get trainer class number
-	ld a, [wCurOpponent]
-	sub OPP_ID_OFFSET
-	ld b, a
-	ld hl, TeamMoves
+; tr_moves loading
+; flag check
+	ld a, [wEnemyPartyFlags]
+	and TRAINERTYPE_MOVES
+	jr z, .noMoves
 
-; iterate through entries in TeamMoves, checking each for our trainer class
-.IterateTeamMoves
-	ld a, [hli]
-	cp b
-	jr z, .GiveTeamMoves ; is there a match?
-	inc hl ; if not, go to the next entry
-	inc a
-	jr nz, .IterateTeamMoves
+; actual loading
+	ld a, [wEnemyPartyCount]
+	dec a ; last mon in team
 
-; no matches found. is this trainer champion rival?
-	ld a, b
-	cp RIVAL3
-	jr z, .ChampionRival
-	jr .FinishUp ; nope
-.GiveTeamMoves
-	ld a, [hl]
-	ld [wEnemyMon5Moves + 2], a
-	jr .FinishUp
-.ChampionRival ; give moves to his team
+	push hl
+	ld hl, wEnemyMon1Moves
+	ld bc, wEnemyMon2 - wEnemyMon1
+	call AddNTimes
+	ld d, h
+	ld e, l
+	pop hl
 
-; pidgeot
-	ld a, SKY_ATTACK
-	ld [wEnemyMon1Moves + 2], a
+	ld b, NUM_MOVES
+.copyMoves
+	call GetNextTrainerDataByte
+	ld [de], a
+	inc de
+	dec b
+	jr nz, .copyMoves
 
-; starter
-	ld a, [wRivalStarter]
-	cp STARTER3
-	ld b, MEGA_DRAIN
-	jr z, .GiveStarterMove
-	cp STARTER1
-	ld b, FIRE_BLAST
-	jr z, .GiveStarterMove
-	ld b, BLIZZARD ; must be squirtle
-.GiveStarterMove
-	ld a, b
-	ld [wEnemyMon6Moves + 2], a
+.noMoves
+	jr .LoopTrainerData
 .FinishUp
 ; clear wAmountMoneyWon addresses
 	xor a

We can now remove the unused data from engine/battle/trainer_ai.asm:

 INCLUDE "engine/battle/read_trainer_party.asm"
 
-INCLUDE "data/trainers/special_moves.asm"
-
 INCLUDE "data/trainers/parties_pointers.asm"
 
 TrainerAI:

And now remove data/trainers/special_moves.asm that became useless.

I'm not gonna lie, it felt good to remove that special_moves code. Now adding moves to parties is way easier.

Support for implicit moves is left as an exercise for you to implement. I may add it later.

If we look at the example from earlier, here is what we can do now:

NewRivalData:
	def_trainer_class NEW_RIVAL
	def_trainer 1
	tr_mon 12, NIDORAN_F
		; tr_nick "Nido"
		tr_moves GROWL, TACKLE, SCRATCH, DOUBLE_KICK
	tr_mon 10, JIGGLYPUFF
		; tr_nick "Jiggly"
		tr_moves SING, POUND
	tr_mon 10, CLEFAIRY
		; tr_nick "Clefy"
		tr_moves POUND, GROWL, METRONOME
	tr_mon 13, SQUIRTLE
		; tr_nick "Blasty"
		tr_moves TACKLE, TAIL_WHIP, BUBBLE, WITHDRAW
		; tr_dvs $FF, $FF
		; tr_stat_exp 2560, 0, 0, 0, 0 ; 1 HP vitamin
	tr_mon 15, DITTO
		; tr_nick "Ditty"
		tr_moves TRANSFORM
	end_trainer

6. Add support for custom DVs per mon

Trainers all had DVs of 9 in attack and 8 for the rest in gen 1, let's change that on a per mon basis.

Let's start by adding a flag:

 ; Trainer party types (see engine/battle/read_trainer_party.asm)
 	const_def
 	shift_const TRAINERTYPE_MOVES     ; bit 0
+	shift_const TRAINERTYPE_DVS       ; bit 1
 ; legacy value that is not simple flags
 DEF TRAINERTYPE_MULTI_LEVELS EQU $FF

Then let's edit macros.

Add default DVs value:

 	for i, 1, NUM_MOVES + 1
 		def _tr_pk{d:p}_move{d:i} = NO_MOVE
 	endr
+	redef _tr_pk{d:p}_dvs EQUS "ATKDEFDV_TRAINER, SPDSPCDV_TRAINER"
 
 	if _tr_lv == TRAINERTYPE_MULTI_LEVELS
 		assert _NARG == 2, "Trainer party requires a level for each mon"

Add a tr_dvs macro to define DVs:

 ENDM
 
+; Usage: tr_dvs <ATK_DEF>, <SPD_SPC>
+MACRO tr_dvs
+	if _NARG != 2
+		fail "A mon needs 2 bytes of DVs"
+	endc
+	def _tr_flags |= TRAINERTYPE_DVS
+	; check if a constant was used
+	if STRFIND("\#", "_") != -1
+		redef _tr_pk{d:p}_dvs EQUS "{\#}"
+	else
+		redef _tr_pk{d:p}_dvs EQUS "\#"
+	endc
+ENDM
+

Include DVs in size:

 		def _tr_size += NUM_MOVES
 	endc
 
+	if _tr_flags & TRAINERTYPE_DVS
+		def _tr_size += 2
+	endc
+
 	def _tr_size *= _tr_mons

Write them in party:

 		endc
+
+		if _tr_flags & TRAINERTYPE_DVS
+			db _tr_pk{d:p}_dvs
+		endc
 	endr
 ENDM

Now let's read the data in engine/battle/read_trainer_party.asm
.FinishUp will end up too far for jr:

 	pop bc
 	ld a, l
 	sub c ; have we reached the end of the trainer data?
-	jr z, .FinishUp
+	jp z, .FinishUp
 	push bc
 
 	call GetNextTrainerDataByte

Add handling for new flag:

 	jr nz, .copyMoves
 
 .noMoves
-	jr .LoopTrainerData
+
+; tr_dvs loading
+; flag check
+	ld a, [wEnemyPartyFlags]
+	and TRAINERTYPE_DVS
+	jr z, .noDVs
+
+; actual loading
+	ld a, [wEnemyPartyCount]
+	dec a ; last mon in team
+
+	push hl
+	ld hl, wEnemyMon1DVs
+	ld bc, wEnemyMon2 - wEnemyMon1
+	call AddNTimes
+	ld d, h
+	ld e, l
+	pop hl
+
+	call GetNextTrainerDataByte
+	ld [de], a
+	inc de
+	call GetNextTrainerDataByte
+	ld [de], a
+
+.noDVs
+
+	ld a, [wEnemyPartyFlags]
+	and TRAINERTYPE_DVS ; | TRAINERTYPE_STAT_EXP
+	jr z, .LoopTrainerData
+
+	push hl
+
+	ld a, [wEnemyPartyCount]
+	dec a ; last mon in team
+	ld hl, wEnemyMon1MaxHP
+	ld bc, wEnemyMon2 - wEnemyMon1
+	call AddNTimes
+	ld d, h
+	ld e, l
+
+	ld a, [wEnemyPartyCount]
+	dec a ; last mon in team
+	ld hl, wEnemyMon1HPExp - 1
+	call AddNTimes
+
+	ld b, TRUE
+	push de
+	call CalcStats
+
+	ld a, [wEnemyPartyCount]
+	dec a ; last mon in team
+	ld hl, wEnemyMon1HP
+	ld bc, wEnemyMon2 - wEnemyMon1
+	call AddNTimes
+	ld d, h
+	ld e, l
+	pop hl
+
+; copy max HP into HP
+	ld a, [hli]
+	ld [de], a
+	inc de
+	ld a, [hl]
+	ld [de], a
+
+	pop hl
+
+	jp .LoopTrainerData
 .FinishUp
 ; clear wAmountMoneyWon addresses
 	xor a

Note that we needed to recalculate stats since those were already calculated by AddPartyMon.

Finally let's modify LoadEnemyMonData in engine/battle/core.asm:

 	jr nz, .storeDVs
 	ld a, [wIsInBattle]
 	cp $2 ; is it a trainer battle?
-; fixed DVs for trainer mon
-	ld a, ATKDEFDV_TRAINER
-	ld b, SPDSPCDV_TRAINER
-	jr z, .storeDVs
+	jr nz, .randomDVs
+; load DVs from party data instead of using fixed ATKDEFDV_TRAINER and SPDSPCDV_TRAINER DVs
+	ld hl, wEnemyMon1DVs
+	ld a, [wWhichPokemon]
+	ld bc, wEnemyMon2 - wEnemyMon1
+	call AddNTimes
+	ld a, [hli]
+	ld b, [hl]
+	jr .storeDVs
+.randomDVs
 ; random DVs for wild mon
 	call BattleRandom
 	ld b, a

If we look at the example from earlier, here is what we can do now:

NewRivalData:
	def_trainer_class NEW_RIVAL
	def_trainer 1
	tr_mon 12, NIDORAN_F
		; tr_nick "Nido"
		tr_moves GROWL, TACKLE, SCRATCH, DOUBLE_KICK
	tr_mon 10, JIGGLYPUFF
		; tr_nick "Jiggly"
		tr_moves SING, POUND
	tr_mon 10, CLEFAIRY
		; tr_nick "Clefy"
		tr_moves POUND, GROWL, METRONOME
	tr_mon 13, SQUIRTLE
		; tr_nick "Blasty"
		tr_moves TACKLE, TAIL_WHIP, BUBBLE, WITHDRAW
		tr_dvs $FF, $FF
		; tr_stat_exp 2560, 0, 0, 0, 0 ; 1 HP vitamin
	tr_mon 15, DITTO
		; tr_nick "Ditty"
		tr_moves TRANSFORM
	end_trainer

Now that DVs are supported, time to move to the next part.

7. Add support for custom stat exp per mon

Now that we can customize DVs, the next step is to support stat exp.

As usual, let's add a new flag:

 ; Trainer party types (see engine/battle/read_trainer_party.asm)
 	const_def
 	shift_const TRAINERTYPE_MOVES     ; bit 0
 	shift_const TRAINERTYPE_DVS       ; bit 1
+	shift_const TRAINERTYPE_STAT_EXP  ; bit 2
 ; legacy value that is not simple flags
 DEF TRAINERTYPE_MULTI_LEVELS EQU $FF

Now, let's add support for it in macros.

First a default value:

 		def _tr_pk{d:p}_move{d:i} = NO_MOVE
 	endr
 	redef _tr_pk{d:p}_dvs EQUS "ATKDEFDV_TRAINER, SPDSPCDV_TRAINER"
+	redef _tr_pk{d:p}_stat_exp EQUS "0, 0, 0, 0, 0"
 
 	if _tr_lv == TRAINERTYPE_MULTI_LEVELS
 		assert _NARG == 2, "Trainer party requires a level for each mon"

Next a new tr_stat_exp macro:

	def _tr_pk{d:p}_dvs_explicit = TRUE
 ENDM
 
+; Usage: tr_stat_exp <HP>, <ATK>, <DEF>, <SPD>, <SPC>
+MACRO tr_stat_exp
+	if _NARG != NUM_STATS
+		fail "A mon needs {d:NUM_STATS} words of stat exp"
+	endc
+	def _tr_flags |= TRAINERTYPE_STAT_EXP
+	; check if a constant was used
+	if STRFIND("\#", "_") != -1
+		redef _tr_pk{d:p}_stat_exp EQUS "{\#}"
+	else
+		redef _tr_pk{d:p}_stat_exp EQUS "\#"
+	endc
+	def _tr_pk{d:p}_stat_exp_explicit = TRUE
+ENDM
+
 ; Write out the party data from stored trainer buffer.
 MACRO end_trainer
 	; First, write the byte length of the party.

Let's include it in size:

 		def _tr_size += 2
 	endc
 
+	if _tr_flags & TRAINERTYPE_STAT_EXP
+		def _tr_size += NUM_STATS * 2
+	endc
+
 	def _tr_size *= _tr_mons
 
 	; Trainer flags

Let's include it in party data as well:

 		if _tr_flags & TRAINERTYPE_DVS
 			db _tr_pk{d:p}_dvs
 		endc
+
+		if _tr_flags & TRAINERTYPE_STAT_EXP
+			bigdw {_tr_pk{d:p}_stat_exp}
+		endc
 	endr
 ENDM

For this to work, edit bigdw in macros/data.asm to support multiple arguments:

 ENDM
 
 MACRO bigdw ; big-endian word
-	db HIGH(\1), LOW(\1)
+	REPT _NARG
+		db HIGH(\1), LOW(\1)
+		SHIFT
+	ENDR
 ENDM
 
 MACRO dba ; dbw bank, address

Now, we need to read that data, let's go in engine/battle/read_trainer_party.asm for that.

Let's add reading code and recalculate stats when the flag is set:

 .noDVs
 
+; tr_stat_exp loading
+; flag check
+	ld a, [wEnemyPartyFlags]
+	and TRAINERTYPE_STAT_EXP
+	jr z, .noStatExp
+
+; actual loading
+	ld a, [wEnemyPartyCount]
+	dec a ; last mon in team
+
+	push hl
+	ld hl, wEnemyMon1HPExp
+	ld bc, wEnemyMon2 - wEnemyMon1
+	call AddNTimes
+	ld d, h
+	ld e, l
+	pop hl
+
+	ld b, NUM_STATS * 2
+.copyStatExp
+	call GetNextTrainerDataByte
+	ld [de], a
+	inc de
+	dec b
+	jr nz, .copyStatExp
+
+.noStatExp
+
 	ld a, [wEnemyPartyFlags]
-	and TRAINERTYPE_DVS ; | TRAINERTYPE_STAT_EXP
-	jr z, .LoopTrainerData
+	and TRAINERTYPE_DVS | TRAINERTYPE_STAT_EXP
+	jp z, .LoopTrainerData
 
 	push hl
 

Now, we need to load them when a mon is sent. Let's edit LoadEnemyMonData in engine/battle/core.asm again:

 	ld a, [wCurEnemyLevel]
 	ld [de], a
 	inc de
-	ld b, $0
+	ld a, [wIsInBattle]
+	dec a
+	ld b, a ; loads 0 for wild mons, gets discarded otherwise
+	jr z, .wildMonCalcStats
+	ld hl, wEnemyMon1MaxHP
+	ld a, [wWhichPokemon]
+	ld bc, wEnemyMon2 - wEnemyMon1
+	call AddNTimes
+	ld b, wEnemyMonSpecial - wEnemyMonMaxHP + 2
+.copyEnemyMonStats
+	ld a, [hli]
+	ld [de], a
+	inc de
+	dec b
+	jr z, .copyHPAndStatusFromPartyData
+	jr .copyEnemyMonStats
+.wildMonCalcStats
 	ld hl, wEnemyMonHP
 	push hl
 	call CalcStats
 	pop hl
-	ld a, [wIsInBattle]
-	cp $2 ; is it a trainer battle?
-	jr z, .copyHPAndStatusFromPartyData
 	ld a, [wEnemyBattleStatus3]
 	bit TRANSFORMED, a ; is enemy mon transformed?
 	jr nz, .copyTypes ; if transformed, jump

If we look at the example from earlier, here is what we can do now:

NewRivalData:
	def_trainer_class NEW_RIVAL
	def_trainer 1
	tr_mon 12, NIDORAN_F
		; tr_nick "Nido"
		tr_moves GROWL, TACKLE, SCRATCH, DOUBLE_KICK
	tr_mon 10, JIGGLYPUFF
		; tr_nick "Jiggly"
		tr_moves SING, POUND
	tr_mon 10, CLEFAIRY
		; tr_nick "Clefy"
		tr_moves POUND, GROWL, METRONOME
	tr_mon 13, SQUIRTLE
		; tr_nick "Blasty"
		tr_moves TACKLE, TAIL_WHIP, BUBBLE, WITHDRAW
		tr_dvs $FF, $FF
		tr_stat_exp 2560, 0, 0, 0, 0 ; 1 HP vitamin
	tr_mon 15, DITTO
		; tr_nick "Ditty"
		tr_moves TRANSFORM
	end_trainer

Now that stat exp is supported, let's move to the next feature.

8. Add support for mon nicknames

You're the only one allowed to nickname their mons, it's unfair! Let's change that.

Now it's time to add the last flag:

 ; Trainer party types (see engine/battle/read_trainer_party.asm)
 	const_def
 	shift_const TRAINERTYPE_MOVES     ; bit 0
 	shift_const TRAINERTYPE_DVS       ; bit 1
 	shift_const TRAINERTYPE_STAT_EXP  ; bit 2
+	shift_const TRAINERTYPE_NICKNAMES ; bit 3
 ; legacy value that is not simple flags
 DEF TRAINERTYPE_MULTI_LEVELS EQU $FF

Add support for it to macros.
Default value:

 	endr
 	redef _tr_pk{d:p}_dvs EQUS "ATKDEFDV_TRAINER, SPDSPCDV_TRAINER"
 	redef _tr_pk{d:p}_stat_exp EQUS "0, 0, 0, 0, 0"
+	redef _tr_pk{d:p}_nickname EQUS ""
 
 	if _tr_lv == TRAINERTYPE_MULTI_LEVELS
 		assert _NARG == 2, "Trainer party requires a level for each mon"

New tr_nick macro:

 	def _tr_pk{d:p}_stat_exp_explicit = TRUE
 ENDM
 
+; Usage: tr_nick NICKNAME
+; Adds a nickname to current mon
+; NICKNAME is formatted as "TEXT". Terminator ("@") is implicit.
+MACRO tr_nick
+	def _tr_flags |= TRAINERTYPE_NICKNAMES
+	redef _tr_pk{d:p}_nickname EQUS \1
+	def _tr_nick_lengths += CHARLEN(\1)
+ENDM
+
 ; Write out the party data from stored trainer buffer.
 MACRO end_trainer
 	; First, write the byte length of the party.

Include it in size:

 		def _tr_size += NUM_STATS * 2
 	endc
 
+	if _tr_flags & TRAINERTYPE_NICKNAMES
+		def _tr_size += 1 ; terminator bytes
+	endc
+
 	def _tr_size *= _tr_mons
 
+	def _tr_size += _tr_nick_lengths
+
 	; Trainer flags
 	def _tr_size += 1
 

Include it in party data:

 		if _tr_flags & TRAINERTYPE_STAT_EXP
 			dw _tr_pk{d:p}_stat_exp
 		endc
+
+		if _tr_flags & TRAINERTYPE_NICKNAMES
+			db "{_tr_pk{d:p}_nickname}@"
+		endc
 	endr
 ENDM

Now, let's add code to read it in engine/battle/read_trainer_party.asm:

 .noStatExp
 
+; tr_nick loading
+; flag check
+	ld a, [wEnemyPartyFlags]
+	and TRAINERTYPE_NICKNAMES
+	jr z, .noNicks
+
+; actual loading
+	ld a, [wEnemyPartyCount]
+	dec a ; last mon in team
+
+	push hl
+	ld hl, wEnemyMonNicks
+	ld bc, wEnemyMon2Nick - wEnemyMon1Nick
+	call AddNTimes
+	ld d, h
+	ld e, l
+	pop hl
+
+.nickCopyLoop
+	call GetNextTrainerDataByte
+	ld [de], a
+	inc de
+	cp "@"
+	jr nz, .nickCopyLoop
+
+.noNicks
+
 	ld a, [wEnemyPartyFlags]
 	and TRAINERTYPE_DVS | TRAINERTYPE_STAT_EXP
 	jp z, .LoopTrainerData

Now we need to read it when sending the mon in battle. To do so, we'll once again edit LoadEnemyMonData in engine/battle/core.asm:

 	inc de
 	ld a, [hl]     ; base exp
 	ld [de], a
+; load nick from party data during trainer battle
+	ld a, [wIsInBattle]
+	dec a
+	jr z, .useSpeciesName
+	ld a, [wEnemyPartyFlags]
+	and TRAINERTYPE_NICKNAMES
+	jr z, .useSpeciesName
+	ld a, [wWhichPokemon]
+	ld hl, wEnemyMon1Nick
+	ld bc, wEnemyMon2Nick - wEnemyMon1Nick
+	call AddNTimes
+	ld a, [hl]
+	cp "@" ; use species name when mon has no nickname
+	jr nz, .readyToLoadName
+.useSpeciesName
 	ld a, [wEnemyMonSpecies2]
 	ld [wNamedObjectIndex], a
 	call GetMonName
 	ld hl, wNameBuffer
+.readyToLoadName
 	ld de, wEnemyMonNick
 	ld bc, NAME_LENGTH
 	call CopyData

If we look at the example from earlier, here is what we can do now:

NewRivalData:
	def_trainer_class NEW_RIVAL
	def_trainer 1
	tr_mon 12, NIDORAN_F
		tr_nick "Nido"
		tr_moves GROWL, TACKLE, SCRATCH, DOUBLE_KICK
	tr_mon 10, JIGGLYPUFF
		tr_nick "Jiggly"
		tr_moves SING, POUND
	tr_mon 10, CLEFAIRY
		tr_nick "Clefy"
		tr_moves POUND, GROWL, METRONOME
	tr_mon 13, SQUIRTLE
		tr_nick "Blasty"
		tr_moves TACKLE, TAIL_WHIP, BUBBLE, WITHDRAW
		tr_dvs $FF, $FF
		tr_stat_exp 2560, 0, 0, 0, 0 ; 1 HP vitamin
	tr_mon 15, DITTO
		tr_nick "Ditty"
		tr_moves TRANSFORM
	end_trainer

Other trainers are finally allowed to nickname their mons. Now it's time to harden our macros, they'll have higher defense against misuse.

9. Add checks to avoid mistakes when using the macros

Let's make our macros more error proof.

Prevent defining a new trainer party before finishing the current one

Let's track def_trainer and end_trainer usage to make sure no trainer gets accidentally skipped.

Define tracking variable and protect last trainer in class in def_trainer_class:

 DEF _tr_class = 0
+DEF _tr_def_in_progress = FALSE
 
 ; Usage: def_trainer_class <CLASS_CONSTANT>
 ; CLASS_CONSTANT is defined in trainer_constants.asm
 MACRO def_trainer_class
+	if _tr_def_in_progress
+		fail "Can't define a new trainer class before finshing the current trainer with end_trainer"
+	endc
 	assert \1 == _tr_class, "Trainer class ID mismatch"
 	def _tr_class += 1
 	def _tr_party = 1
 ENDM

Protect other trainers in def_trainer:

 ; TRAINER_INDEX is 1-based
 ; PARTY_LEVEL is the level for the whole party, defaults to TRAINERTYPE_MULTI_LEVELS to set mon levels individually
 MACRO def_trainer
+	if _tr_def_in_progress
+		fail "Can't define a new trainer before finishing the current one with end_trainer"
+	endc
 	; Reset trainer macro state.
 	def _tr_flags = 0
 	def _tr_mons = 0

Mark trainer definition as in progress in def_trainer:

 	endc
 	def _tr_size = 0
 	def _tr_party += 1
+	def _tr_def_in_progress = TRUE
 ENDM
 
 ; Usage: tr_mon [LEVEL,] <SPECIES>

Mark trainer definition as complete in end_trainer and protect last trainer and its class with a new end_trainer_parties macro:

 			db "{_tr_pk{d:p}_nickname}@"
 		endc
 	endr
+	def _tr_def_in_progress = FALSE
 ENDM
+
+MACRO end_trainer_parties
+	if _tr_def_in_progress
+		fail "Can't end trainer parties without finishing the last trainer with end_trainer"
+	endc
+	if _tr_class != NUM_TRAINERS + 1
+		fail "Number of trainer classes doesn't match the number of def_trainer_class calls"
+	endc
+ENDM

Use the new macro at the end of data/trainers/parties.asm:

 	tr_mon 62, DRAGONITE
 		tr_moves AGILITY, SLAM, BARRIER, HYPER_BEAM
 	end_trainer
+
+end_trainer_parties

Validate nickname length

Add a check to prevent leaking nicknames into the rest of WRAM:

 ; NICKNAME is formatted as "TEXT". Terminator ("@") is implicit.
 MACRO tr_nick
 	def _tr_flags |= TRAINERTYPE_NICKNAMES
+	def _tr_curr_nick_len = CHARLEN(\1)
+	assert fail, _tr_curr_nick_len < NAME_LENGTH, "Nickname \1 is too long, it should be less than {d:NAME_LENGTH} bytes long but is {d:_tr_curr_nick_len} bytes long"
 	redef _tr_pk{d:p}_nickname EQUS \1
-	def _tr_nick_lengths += CHARLEN(\1)
+	def _tr_nick_lengths += _tr_curr_nick_len
 ENDM
 
 ; Write out the party data from stored trainer buffer.

The rest of this tutorial is optional but highly recommended anyway.

10. Going further

We have a minimal working customizable party system and could just stop here but using it as is will highlight its shortcomings. The improvement will involve more complex macros so if you're already uneasy with the ones used so far, take a break and review your knowledge of macros.

We'll start with an easier change with simple macros.

10.1 Improving the code that points to correct mon data

When reading the party data, we do a lot of loading the current party size in a to decrement it right after and call AddNTimes. We don't need to decrement it if we offset hl to account for the off by one index. Let's improve that with macros.

We'll start by adding a simple macro to the end of macros/code.asm

 	ENDC
 	jp z, \2
 ENDM
+
+; Avoids dec a, then AddNTimes
+; Usage: add_n_times_1_based DataPointer, DATA_SIZE
+MACRO add_n_times_1_based
+	ld hl, (\1) - (\2)
+	ld bc, \2
+	call AddNTimes
+ENDM

Then let's use in a new macros/read_trainer_party.asm file.

+; Point to last added mon data
+; Usage: point_mon_data wEnemyMon1Part, [DATA_SIZE]
+MACRO point_mon_data
+	if _NARG == 1
+		redef _data_size EQUS "wEnemyMon2 - wEnemyMon1"
+	else
+		redef _data_size EQUS "\2"
+	endc
+	ld a, [wEnemyPartyCount]
+
+	push hl
+	add_n_times_1_based \1, _data_size
+	ld d, h
+	ld e, l
+	pop hl
+ENDM

Let's make this macro usable by including it in includes.asm

 INCLUDE "macros/scripts/events.asm"
 INCLUDE "macros/scripts/text.asm"
 
+INCLUDE "macros/read_trainer_party.asm"
+
 INCLUDE "constants/charmap.asm"
 INCLUDE "constants/hardware_constants.asm"
 INCLUDE "constants/oam_constants.asm"

Let's use the macro in engine/battle/read_trainer_party.asm
Starting with the moves reading part:

 	jr z, .noMoves
 
 ; actual loading
-	ld a, [wEnemyPartyCount]
-	dec a ; last mon in team
-
-	push hl
-	ld hl, wEnemyMon1Moves
-	ld bc, wEnemyMon2 - wEnemyMon1
-	call AddNTimes
-	ld d, h
-	ld e, l
-	pop hl
+	point_mon_data wEnemyMon1Moves
 
 	ld b, NUM_MOVES
 .copyMoves

Continuing with the DVs reading part:

 	jr z, .noDVs
 
 ; actual loading
-	ld a, [wEnemyPartyCount]
-	dec a ; last mon in team
-
-	push hl
-	ld hl, wEnemyMon1DVs
-	ld bc, wEnemyMon2 - wEnemyMon1
-	call AddNTimes
-	ld d, h
-	ld e, l
-	pop hl
+	point_mon_data wEnemyMon1DVs
 
 	call GetNextTrainerDataByte
 	ld [de], a

Then the stat exp reading part:

 	jr z, .noStatExp
 
 ; actual loading
-	ld a, [wEnemyPartyCount]
-	dec a ; last mon in team
-
-	push hl
-	ld hl, wEnemyMon1HPExp
-	ld bc, wEnemyMon2 - wEnemyMon1
-	call AddNTimes
-	ld d, h
-	ld e, l
-	pop hl
+	point_mon_data wEnemyMon1HPExp
 
 	ld b, NUM_STATS * 2
 .copyStatExp

Now the nicknames part:

 	jr z, .noNicks
 
 ; actual loading
-	ld a, [wEnemyPartyCount]
-	dec a ; last mon in team
-
-	push hl
-	ld hl, wEnemyMonNicks
-	ld bc, wEnemyMon2Nick - wEnemyMon1Nick
-	call AddNTimes
-	ld d, h
-	ld e, l
-	pop hl
+	point_mon_data wEnemyMonNicks, wEnemyMon2Nick - wEnemyMon1Nick
 
 .nickCopyLoop
 	call GetNextTrainerDataByte

Finally, the recalculate stats part

 	push hl
 
 	ld a, [wEnemyPartyCount]
-	dec a ; last mon in team
-	ld hl, wEnemyMon1MaxHP
-	ld bc, wEnemyMon2 - wEnemyMon1
-	call AddNTimes
+	add_n_times_1_based wEnemyMon1MaxHP, wEnemyMon2 - wEnemyMon1
 	ld d, h
 	ld e, l
 
 	ld a, [wEnemyPartyCount]
-	dec a ; last mon in team
-	ld hl, wEnemyMon1HPExp - 1
+	ld hl, wEnemyMon1HPExp - 1 - (wEnemyMon2 - wEnemyMon1)
 	call AddNTimes
 
 	ld b, TRUE
 	push de
 	call CalcStats
 
 	ld a, [wEnemyPartyCount]
-	dec a ; last mon in team
-	ld hl, wEnemyMon1HP
-	ld bc, wEnemyMon2 - wEnemyMon1
-	call AddNTimes
+	add_n_times_1_based wEnemyMon1HP, wEnemyMon2 - wEnemyMon1
 	ld d, h
 	ld e, l
 	pop hl

We're not using the macro for the middle diff because bc doesn't change

Now we'll tackle the big change

10.2 Improving DVs and stat exp macros

After using them or seeing the example usage, you probably noticed that the tr_dvs and tr_stat_exp macros, despite being useful, are tedious to use. You have to specify all values when calling them and $98, $88 is harder to read than 8 all, 9 atk.

Let's work on improving them.

10.2.1 Improve tr_dvs and tr_stat_exp input

We're going to borrow macros from Polished Crystal yet again. These one are more complex so take your time reading them and their comments.

We'll first add a new macro and new defines in macros/data.asm:

 		SHIFT
 	ENDR
 ENDM
+
+MACRO with_each ; with_each FOO, BAR, "DEF BASE_? = 42"
+	for _element, 1, _NARG ; iterates with FOO and BAR
+		redef _eval_buffer EQUS STRRPL(\<_NARG>, "?", "\<_element>") ; redef _eval_buffer EQUS STRRPL("DEF BASE_? = 42", "?", "FOO") ; redef _eval_buffer EQUS STRRPL("DEF BASE_? = 42", "?", "BAR")
+		{_eval_buffer} ; DEF BASE_FOO = 42 ; DEF BASE_BAR = 42
+	endr
+ENDM
+
+DEF with_each_dv_stat EQUS "with_each ATK, DEF, SPD, SPC,"
+DEF with_each_stat EQUS "with_each HP, ATK, DEF, SPD, SPC,"
+DEF with_each_stat_all EQUS "with_each ALL, HP, ATK, DEF, SPD, SPC,"

Now it's time to use them in data/trainers/macros.asm by adding new macros between tr_nick and end_trainer:

 	def _tr_nick_lengths += _tr_curr_nick_len
 ENDM
 
+; Internal, shared by DVs and stat exp
+MACRO def_stat_props
+	rept _NARG
+		def _got_stat = 0
+		with_each_stat_all """
+			def x = STRFIND(STRUPR("\1"), " ?")
+			if !_got_stat && x != -1 && (!ignore_hp || STRCMP("?", "HP"))
+				redef _eval_stat EQUS STRSLICE("\1", 0, x)
+				def ?_STAT_PROP = \{_eval_stat}
+				if ?_STAT_PROP <= MAX_STAT_PROP
+					def _got_stat = 1
+				endc
+			endc
+			"""
+		if !_got_stat
+			fail "invalid {prop_name} \1"
+		endc
+		if ALL_STAT_PROP != 0
+			with_each_stat "def ?_STAT_PROP = {ALL_STAT_PROP}"
+			def ALL_STAT_PROP = 0 ; allows stuff like "8 all, 9 atk"
+		endc
+		shift
+	endr
+ENDM
+
+DEF DV_SPREADS_COUNT = 0
+DEF STAT_EXP_SPREADS_COUNT = 0
+DEF MONS_WITH_DV_SPREAD = 0
+DEF MONS_WITH_STAT_EXP_SPREAD = 0
+
+; Internal
+MACRO def_dvs
+	def MAX_STAT_PROP = 15
+	with_each_dv_stat "def ?_STAT_PROP = 8"
+	def ATK_STAT_PROP = 9
+	def ALL_STAT_PROP = 0
+	redef prop_name EQUS "DV"
+	def ignore_hp = 1
+	def_stat_props \#
+ENDM
+
+; Internal
+MACRO dv_spread
+	def_dvs \#
+	if !def(DV_SPREAD_FOR_{d:ATK_STAT_PROP}_{d:DEF_STAT_PROP}_{d:SPD_STAT_PROP}_{d:SPC_STAT_PROP})
+		def DV_SPREAD_FOR_{d:ATK_STAT_PROP}_{d:DEF_STAT_PROP}_{d:SPD_STAT_PROP}_{d:SPC_STAT_PROP} = DV_SPREADS_COUNT
+		redef DV_SPREADS_COUNT += 1
+	endc
+	dn {d:ATK_STAT_PROP}, {d:DEF_STAT_PROP}, {d:SPD_STAT_PROP}, {d:SPC_STAT_PROP}
+	redef MONS_WITH_DV_SPREAD += 1
+ENDM
+
+; Internal
+MACRO def_stat_exp
+	def MAX_STAT_PROP = $ffff
+	with_each_stat_all "def ?_STAT_PROP = 0"
+	redef prop_name EQUS "stat exp"
+	def ignore_hp = 0
+	def_stat_props \#
+ENDM
+
+; Internal
+MACRO stat_exp_spread
+	def_stat_exp \#
+	if !def(STAT_EXP_SPREAD_FOR_{d:HP_STAT_PROP}_{d:ATK_STAT_PROP}_{d:DEF_STAT_PROP}_{d:SPD_STAT_PROP}_{d:SPC_STAT_PROP})
+		def STAT_EXP_SPREAD_FOR_{d:HP_STAT_PROP}_{d:ATK_STAT_PROP}_{d:DEF_STAT_PROP}_{d:SPD_STAT_PROP}_{d:SPC_STAT_PROP} = STAT_EXP_SPREADS_COUNT
+		redef STAT_EXP_SPREADS_COUNT += 1
+	endc
+	bigdw {d:HP_STAT_PROP}, {d:ATK_STAT_PROP}, {d:DEF_STAT_PROP}, {d:SPD_STAT_PROP}, {d:SPC_STAT_PROP}
+	redef MONS_WITH_STAT_EXP_SPREAD += 1
+ENDM
+
 ; Write out the party data from stored trainer buffer.
 MACRO end_trainer
 	; First, write the byte length of the party.

Take time to read them, keep in mind that they're not used yet, do not hesitate to read their usage below if you have trouble understanding how they would be used.

Let's use them, still in the same file, in the end_trainer macro:

 		endc
 
 		if _tr_flags & TRAINERTYPE_DVS
-			db _tr_pk{d:p}_dvs
+			dv_spread {_tr_pk{d:p}_dvs}
 		endc
 
 		if _tr_flags & TRAINERTYPE_STAT_EXP
-			bigdw {_tr_pk{d:p}_stat_exp}
+			stat_exp_spread {_tr_pk{d:p}_stat_exp}
 		endc
 
 		if _tr_flags & TRAINERTYPE_NICKNAMES

Now we need to change the default values and let the tr_dvs and tr_stat_exp accept the new syntax.
default values:

 	for i, 1, NUM_MOVES + 1
 		def _tr_pk{d:p}_move{d:i} = NO_MOVE
 	endr
-	redef _tr_pk{d:p}_dvs EQUS "ATKDEFDV_TRAINER, SPDSPCDV_TRAINER"
-	redef _tr_pk{d:p}_stat_exp EQUS "0, 0, 0, 0, 0"
+	redef _tr_pk{d:p}_dvs EQUS "8 all, 9 atk"
+	redef _tr_pk{d:p}_stat_exp EQUS "0 all"
 	redef _tr_pk{d:p}_nickname EQUS ""
 
 	if _tr_lv == TRAINERTYPE_MULTI_LEVELS

tr_dvs:

 	endr
 ENDM
 
-; Usage: tr_dvs <ATK_DEF>, <SPD_SPC>
+; Usage: tr_dvs n1 ALL|ATK|DEF|SPD|SPC, [n2 ATK|DEF|SPD|SPC]
 MACRO tr_dvs
-	if _NARG != 2
-		fail "A mon needs 2 bytes of DVs"
-	endc
 	def _tr_flags |= TRAINERTYPE_DVS
 	; check if a constant was used
 	if STRFIND("\#", "_") != -1

tr_stat_exp:

 	def _tr_pk{d:p}_dvs_explicit = TRUE
 ENDM
 
-; Usage: tr_stat_exp <HP>, <ATK>, <DEF>, <SPD>, <SPC>
+; Usage: tr_stat_exp n1 ALL|HP|ATK|DEF|SPD|SPC, [n2 HP|ATK|DEF|SPD|SPC]
 MACRO tr_stat_exp
-	if _NARG != NUM_STATS
-		fail "A mon needs {d:NUM_STATS} words of stat exp"
-	endc
 	def _tr_flags |= TRAINERTYPE_STAT_EXP
 	; check if a constant was used
 	if STRFIND("\#", "_") != -1

The inputs are now more user friendly, we can write inputs like:

		tr_dvs 15 all
		; equivalent of modern 252 EVs, 2nd form lets you visualize the amohttps://rgbds.gbdev.io/docs/v0.9.2/rgbasm.5#THE_MACRO_LANGUAGEunt gained at lv 100
		tr_stat_exp 252 ** 2 hp, (63 * 4) ** 2 def

You may wonder why some variables were defined in the but not used. It's because we're about to use them below and in the next part.

Let's modify the end_trainer_parties macro to add some info about the generated data:

 	if _tr_class != NUM_TRAINERS + 1
 		fail "Number of trainer classes doesn't match the number of def_trainer_class calls"
 	endc
+	def _dvs_waste = (MONS_WITH_DV_SPREAD - DV_SPREADS_COUNT) ; spread size is 2 but index for a spread table is 1 so n * (2 - 1) = n * 1 = n
+	println "You defined {d:DV_SPREADS_COUNT} distinct DV spreads for a total of {d:MONS_WITH_DV_SPREAD} mons, which is {d:_dvs_waste} bytes wasted compared to using a spread table"
+	def _stat_exp_waste = (MONS_WITH_STAT_EXP_SPREAD - STAT_EXP_SPREADS_COUNT) * ((NUM_STATS * 2) - 1) ; spread size is 10 but index for a spread table is 1 so n * (10 - 1) = n * 9
+	println "You defined {d:STAT_EXP_SPREADS_COUNT} distinct stat exp spreads for a total of {d:MONS_WITH_STAT_EXP_SPREAD} mons, which is {d:_stat_exp_waste} bytes wasted compared to using a spread table"
+	purge _dvs_waste, _stat_exp_waste
 ENDM

You'll find that data grows quickly and that duplicate spreads take a lot of space. Those messages will help you decide when you want to switch to spread tables.

If we look at the example from earlier, here is what we can do now:

NewRivalData:
	def_trainer_class NEW_RIVAL
	def_trainer 1
	tr_mon 12, NIDORAN_F
		tr_nick "Nido"
		tr_moves GROWL, TACKLE, SCRATCH, DOUBLE_KICK
	tr_mon 10, JIGGLYPUFF
		tr_nick "Jiggly"
		tr_moves SING, POUND
	tr_mon 10, CLEFAIRY
		tr_nick "Clefy"
		tr_moves POUND, GROWL, METRONOME
	tr_mon 13, SQUIRTLE
		tr_nick "Blasty"
		tr_moves TACKLE, TAIL_WHIP, BUBBLE, WITHDRAW
		tr_dvs 15 all
		tr_stat_exp 2560 hp ; 1 HP vitamin
	tr_mon 15, DITTO
		tr_nick "Ditty"
		tr_moves TRANSFORM
	end_trainer

10.2.2 Moving DVs and stat exp into spread tables.

Let's continue the refactoring. We'll reduce the size of the trainer party data for DVs to 1 byte that is going to be an index on a DV spreads table and do the same for the stat exp as well.

Let's start with the macros.
DVs macro:

 	def_dvs \#
 	if !def(DV_SPREAD_FOR_{d:ATK_STAT_PROP}_{d:DEF_STAT_PROP}_{d:SPD_STAT_PROP}_{d:SPC_STAT_PROP})
 		def DV_SPREAD_FOR_{d:ATK_STAT_PROP}_{d:DEF_STAT_PROP}_{d:SPD_STAT_PROP}_{d:SPC_STAT_PROP} = DV_SPREADS_COUNT
+		with_each_dv_stat "DEF DV_SPREAD_{d:DV_SPREADS_COUNT}_? = ?_STAT_PROP"
 		redef DV_SPREADS_COUNT += 1
 	endc
-	dn {d:ATK_STAT_PROP}, {d:DEF_STAT_PROP}, {d:SPD_STAT_PROP}, {d:SPC_STAT_PROP}
+	db DV_SPREAD_FOR_{d:ATK_STAT_PROP}_{d:DEF_STAT_PROP}_{d:SPD_STAT_PROP}_{d:SPC_STAT_PROP}
 	redef MONS_WITH_DV_SPREAD += 1
 ENDM

Stat exp macro:

 	def_stat_exp \#
 	if !def(STAT_EXP_SPREAD_FOR_{d:HP_STAT_PROP}_{d:ATK_STAT_PROP}_{d:DEF_STAT_PROP}_{d:SPD_STAT_PROP}_{d:SPC_STAT_PROP})
 		def STAT_EXP_SPREAD_FOR_{d:HP_STAT_PROP}_{d:ATK_STAT_PROP}_{d:DEF_STAT_PROP}_{d:SPD_STAT_PROP}_{d:SPC_STAT_PROP} = STAT_EXP_SPREADS_COUNT
+		with_each_stat "DEF STAT_EXP_SPREAD_{d:STAT_EXP_SPREADS_COUNT}_? = ?_STAT_PROP"
 		redef STAT_EXP_SPREADS_COUNT += 1
 	endc
-	bigdw {d:HP_STAT_PROP}, {d:ATK_STAT_PROP}, {d:DEF_STAT_PROP}, {d:SPD_STAT_PROP}, {d:SPC_STAT_PROP}
+	db STAT_EXP_SPREAD_FOR_{d:HP_STAT_PROP}_{d:ATK_STAT_PROP}_{d:DEF_STAT_PROP}_{d:SPD_STAT_PROP}_{d:SPC_STAT_PROP}
 	redef MONS_WITH_STAT_EXP_SPREAD += 1
 ENDM

end_trainer macro:

 	endc
 
 	if _tr_flags & TRAINERTYPE_DVS
-		def _tr_size += 2
+		def _tr_size += 1
 	endc
 
 	if _tr_flags & TRAINERTYPE_STAT_EXP
-		def _tr_size += NUM_STATS * 2
+		def _tr_size += 1
 	endc
 
 	if _tr_flags & TRAINERTYPE_NICKNAMES

end_trainer_parties macro:

 	if _tr_class != NUM_TRAINERS + 1
 		fail "Number of trainer classes doesn't match the number of def_trainer_class calls"
 	endc
-	def _dvs_waste = (MONS_WITH_DV_SPREAD - DV_SPREADS_COUNT) ; spread size is 2 but index for a spread table is 1 so n * (2 - 1) = n * 1 = n
-	println "You defined {d:DV_SPREADS_COUNT} distinct DV spreads for a total of {d:MONS_WITH_DV_SPREAD} mons, which is {d:_dvs_waste} bytes wasted compared to using a spread table"
-	def _stat_exp_waste = (MONS_WITH_STAT_EXP_SPREAD - STAT_EXP_SPREADS_COUNT) * ((NUM_STATS * 2) - 1) ; spread size is 10 but index for a spread table is 1 so n * (10 - 1) = n * 9
-	println "You defined {d:STAT_EXP_SPREADS_COUNT} distinct stat exp spreads for a total of {d:MONS_WITH_STAT_EXP_SPREAD} mons, which is {d:_stat_exp_waste} bytes wasted compared to using a spread table"
-	purge _dvs_waste, _stat_exp_waste
+	def _dvs_saved = (MONS_WITH_DV_SPREAD - DV_SPREADS_COUNT) ; spread size is 2 but index for a spread table is 1 so n * (2 - 1) = n * 1 = n
+	println "You defined {d:DV_SPREADS_COUNT} distinct DV spreads for a total of {d:MONS_WITH_DV_SPREAD} mons, which is {d:_dvs_saved} bytes saved by using a spread table"
+	def _stat_exp_saved = (MONS_WITH_STAT_EXP_SPREAD - STAT_EXP_SPREADS_COUNT) * ((NUM_STATS * 2) - 1) ; spread size is 10 but index for a spread table is 1 so n * (10 - 1) = n * 9
+	println "You defined {d:STAT_EXP_SPREADS_COUNT} distinct stat exp spreads for a total of {d:MONS_WITH_STAT_EXP_SPREAD} mons, which is {d:_stat_exp_saved} bytes saved by using a spread table"
+	purge _dvs_saved, _stat_exp_saved
 ENDM

Now, we need to actually create those spread tables.

We'll append the data to data/trainers/parties.asm, in its own section:

 	end_trainer
 
 end_trainer_parties
+
+SECTION "DV and stat exp spreads", ROMX
+
+DVSpreads:
+	table_width 2
+	FOR idx, DV_SPREADS_COUNT
+		dn DV_SPREAD_{d:idx}_ATK, DV_SPREAD_{d:idx}_DEF, DV_SPREAD_{d:idx}_SPD, DV_SPREAD_{d:idx}_SPC
+	ENDR
+	assert_table_length DV_SPREADS_COUNT
+
+StatExpSpreads:
+	table_width NUM_STATS * 2
+	FOR idx, STAT_EXP_SPREADS_COUNT
+		with_each_stat "bigdw STAT_EXP_SPREAD_{d:idx}_?"
+	ENDR
+	assert_table_length STAT_EXP_SPREADS_COUNT

Now all that's left is to correctly read the data.

Let's open engine/battle/read_trainer_party.asm.
Change the way to read the DVs:

 	point_mon_data wEnemyMon1DVs
 
 	call GetNextTrainerDataByte
-	ld [de], a
-	inc de
-	call GetNextTrainerDataByte
-	ld [de], a
+	; a is now the DVSpreads index
+	push hl
+	ld hl, DVSpreads
+	ld bc, 2
+	call AddNTimes
+	; hl now points to the correct DVs spread
+	ld a, BANK(DVSpreads)
+	call FarCopyData2
+	pop hl
 
 .noDVs

Change the way to read stat exp:

 ; actual loading
 	point_mon_data wEnemyMon1HPExp
 
-	ld b, NUM_STATS * 2
-.copyStatExp
 	call GetNextTrainerDataByte
-	ld [de], a
-	inc de
-	dec b
-	jr nz, .copyStatExp
+	; a is now the StatExpSpreads index
+	push hl
+	ld hl, StatExpSpreads
+	ld bc, NUM_STATS * 2
+	call AddNTimes
+	; hl now points to the correct stat exp spread
+	ld a, BANK(StatExpSpreads)
+	call FarCopyData2
+	pop hl
 
 .noStatExp

And that's all there is to change in the reading code, the bulk of the change really was the macros.

10.3 Remove useless instructions

AddPartyMon already preserves hl so the push and pop around the call are redundant:

 	ld [wCurPartySpecies], a
 	ld a, ENEMY_PARTY_DATA
 	ld [wMonDataLocation], a
-	push hl
 	call AddPartyMon
-	pop hl
 
 ; tr_moves loading
 ; flag check

10.4 Out of scope, but related features

While not in the scope of this tutorial, some features are related to trainer parties so I'll mention some of them here:

  • It should be easy to implement trainer names via the def_trainer macro by looking at how pokecrystal handles those.
  • Support for trainer class specific DVs, like gen 2 does, can be a good exercise for you to implement if you want different fallback values for each class.
  • Since flags still have 4 bits available to use, a level scaling feature can be added as a flag to the party if you want only certain trainers to scale.
  • Replacing stat exp with modern EVs needs to update the stat exp macros and code, as well as the spreads table generation

11. Feature branch

I extracted this tutorial from a branch with all those changes on my fork.

Some changes were not made in the same order as in the tutorial because I fixed problems as I stumbled on them, so if you notice any problem I've missed, please notify me. I'll update the branch and the tutorial.

I plan to sync the branch with pret's master regularly.
You are encouraged to use it as a feature branch.

Clone this wiki locally