-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Modify the enemy party loading system to allow nicknames, custom movesets, custom DVs and stat exp per mon (similar to Polished Crystal's system)
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.
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.
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.
- Rewrite party data as macros
- Replace the terminator byte with a leading size byte
- Replace the level byte with flags and include a level byte for every mon
- Make the party data able to be in different banks and move it to its own section
- Include mon moves directly into party data and remove the old way of customizing moves
- Add support for custom DVs per mon
- Add support for custom stat exp per mon
- Add support for mon nicknames
- Add checks to avoid mistakes when using the macros
- Going further
- Feature branch
The pokered party data format is quite simple. The format we aiming to change it to is less simple. Changing db
s manually with each step is going to be very tedious if we don't use macros, so we'll start by replacing the existing db
s 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.
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.
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.
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.
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
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.
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.
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.
Let's make our macros more error proof.
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
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.
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.
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
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.
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
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.
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
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
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.