-
Notifications
You must be signed in to change notification settings - Fork 989
Trainers are not Pokemon
If you tried to implement more than 200 Pokémon, then you probably encountered some weird bugs with trainers being loaded in instead of the new Pokémon. The reason for this is that the ID which identifies the Pokémon species is also used to identify the trainers. The purpose of this tutorial is to provide a workaround to allow for the implementation of more than 200 Pokemon.
This tutorial is a modification of work done in this commit by JustRegularLuna. I, Xillicis, take no credit for the fix itself.
Open up ram/wram.asm and define the following new variables in RAM, replacing the free space already there (marked by ds 2
):
...
wPseudoItemID:: db
wUnusedD153:: db
- ds 2
+wIsTrainerBattle:: db
+
+wWasTrainerBattle:: db
wEvoStoneItemID:: db
wSavedNPCMovementDirections2Index:: db
...
In the code, to check whether a trainer or Pokémon is used, the value of wCurOpponent
is compared against OPP_ID_OFFSET
(which is 200). Instead of this logic, we can simply check the byte at wIsTrainerBattle
to know whether the battle is against a Pokémon (0) or a trainer (1). Therefore, we need to make the following modifications to a host of files.
Open audio/play_battle_music.asm and make the following change to the subroutine PlayBattleMusic
:
...
.notGymLeaderBattle
- ld a, [wCurOpponent]
- cp OPP_ID_OFFSET
- jr c, .wildBattle
+ ld a, [wIsTrainerBattle]
+ and a
+ jr z, .wildBattle
+ ld a, [wCurOpponent]
cp OPP_RIVAL3
jr z, .finalBattle
...
Next open up engine/battle/battle_transitions.asm and make this change:
...
GetBattleTransitionID_WildOrTrainer:
- ld a, [wCurOpponent]
- cp OPP_ID_OFFSET
- jr nc, .trainer
+ ld a, [wIsTrainerBattle]
+ and a
+ jr nz, .trainer
res 0, c
ret
...
Next, open the file engine/battle/core.asm and make the following modification to TrainerBattleVictory
:
...
call PrintEndBattleText
; win money
ld hl, MoneyForWinningText
call PrintText
+
+ xor a
+ ld [wIsTrainerBattle], a
+ inc a
+ ld [wWasTrainerBattle], a
+
ld de, wPlayerMoney + 2
ld hl, wAmountMoneyWon + 2
ld c, $3
...
Head own down about 200 lines or so and make this change:
...
HandlePlayerBlackOut:
+ xor a
+ ld [wIsTrainerBattle], a
ld a, [wLinkState]
cp LINK_STATE_BATTLING
jr z, .notSony1Battle
...
Continue down to InitBattleCommon
which is around line 6780 and make these changes:
...
InitBattleCommon:
ld a, [wMapPalOffset]
push af
ld hl, wLetterPrintingDelayFlags
ld a, [hl]
push af
res 1, [hl]
callfar InitBattleVariables
+ ld a, [wIsTrainerBattle]
+ and a
+ jp z, InitWildBattle
ld a, [wEnemyMonSpecies2]
sub OPP_ID_OFFSET
- jp c, InitWildBattle
ld [wTrainerClass], a
call GetTrainerInformation
...
Next open, engine/battle/wild_encounters.asm and add the following line:
...
and a
ret
.willEncounter
xor a
+ ld [wIsTrainerBattle], a
ret
INCLUDE "data/wild/probabilities.asm"
Now we modify EndTrainerBattle:
in home/trainers.asm.
This subroutine will remove any non-trainer sprite from the overworld; for example, the Voltorbs in the Power Plant or Snorlax on the side of the roads.
We use the variable wWasTrainerBattle
to determine if we should remove the sprite; that is, if the last fight was a trainer fight, then the value stored in wWasTrainerBattle
should be non-zero.
...
ld hl, wFlags_0xcd60
res 0, [hl] ; player is no longer engaged by any trainer
ld a, [wIsInBattle]
cp $ff
- jp z, ResetButtonPressedAndMapScript
+ jr z, EndTrainerBattleWhiteout
ld a, $2
call ReadTrainerHeaderInfo
ld a, [wTrainerHeaderFlagBit]
ld c, a
ld b, FLAG_SET
call TrainerFlagAction ; flag trainer as fought
- ld a, [wEnemyMonOrTrainerClass]
- cp OPP_ID_OFFSET
- jr nc, .skipRemoveSprite ; test if trainer was fought (in that case skip removing the corresponding sprite)
+ ld a, [wWasTrainerBattle]
+ and a
+ jr nz, .skipRemoveSprite ; test if trainer was fought (in that case skip removing the corresponding sprite)
+ ld a, [wCurMap]
+ cp POKEMON_TOWER_7F
+ jr z, .skipRemoveSprite ; the two 7F scripts call EndTrainerBattle manually after wIsTrainerBattle has been unset
ld hl, wMissableObjectList
ld de, $2
ld a, [wSpriteIndex]
call IsInArray ; search for sprite ID
inc hl
ld a, [hl]
ld [wMissableObjectIndex], a ; load corresponding missable object index and remove it
predef HideObject
.skipRemoveSprite
+ xor a
+ ld [wWasTrainerBattle], a
ld hl, wd730
bit 4, [hl]
res 4, [hl]
ret nz
-
-ResetButtonPressedAndMapScript::
+EndTrainerBattleWhiteout::
xor a
+ ld [wIsTrainerBattle], a
+ ld [wWasTrainerBattle], a
ld [wJoyIgnore], a
ldh [hJoyHeld], a
ldh [hJoyPressed], a
ldh [hJoyReleased], a
ld [wCurMapScript], a ; reset battle status
ret
...
Head down a bit further to around and modify InitBattleEnemyParameters
:
...
InitBattleEnemyParameters::
ld a, [wEngagedTrainerClass]
ld [wCurOpponent], a
ld [wEnemyMonOrTrainerClass], a
- cp OPP_ID_OFFSET
+ ld a, [wIsTrainerBattle]
+ and a
ld a, [wEngagedTrainerSet]
- jr c, .noTrainer
+ jr z, .noTrainer
ld [wTrainerNo], a
ret
.noTrainer
ld [wCurEnemyLVL], a
ret
...
And now to EngageMapTrainer
and make these changes:
...
add hl, de ; seek to engaged trainer data
ld a, [hli] ; load trainer class
ld [wEngagedTrainerClass], a
ld a, [hl] ; load trainer mon set
+ bit 7, a
+ jr nz, .pokemon
+ ld [wEngagedTrainerSet], a
+ ld a, 1
+ ld [wIsTrainerBattle], a
+ jp PlayTrainerMusic
+.pokemon
+ and $7F
ld [wEngagedTrainerSet], a
+ xor a
+ ld [wIsTrainerBattle], a
jp PlayTrainerMusic
...
Lastly, the EndTrainerBattle
subroutine is not called for all trainer fights (e.g. Rival fights), therefore, we initialize wWasTrainerBattle
to be zero at the start of every fight.
Make the following modification to InitBattleVariables
in engine/battle/init_battle_variables.asm
InitBattleVariables:
ldh a, [hTileAnimations]
ld [wSavedTileAnimations], a
xor a
+ ld [wWasTrainerBattle], a
ld [wActionResultOrTookBattleTurn], a
ld [wBattleResult], a
ld hl, wPartyAndBillsPCSavedMenuItem
...
Finally, we need to modify all the scripts which handle the Rival fights. This is necessary as all of the Rival fights are manually handled by the script files. We'll just go down the list.
Start with scripts/OaksLab.asm and modify OaksLabRivalStartBattleScript:
and OaksLabRivalEndBattleScript:
OaksLabRivalStartBattleScript:
ld a, [wd730]
bit 0, a
ret nz
; define which team rival uses, and fight it
+ ld a, 1
+ ld [wIsTrainerBattle], a
ld a, OPP_RIVAL1
ld [wCurOpponent], a
...
...
OaksLabRivalEndBattleScript:
+ xor a
+ ld [wIsTrainerBattle], a
ld a, D_RIGHT | D_LEFT | D_UP | D_DOWN
ld [wJoyIgnore], a
...
Then, scripts/Route22.asm:
Route22GetRivalTrainerNoByStarterScript:
ld a, [wRivalStarter]
ld b, a
.next_trainer_no
ld a, [hli]
cp b
jr z, .got_trainer_no
inc hl
jr .next_trainer_no
.got_trainer_no
ld a, [hl]
ld [wTrainerNo], a
+ ld a, 1
+ ld [wIsTrainerBattle], a
ret
Head down a bit further and add:
Route22Rival1AfterBattleScript:
ld a, [wIsInBattle]
cp $ff
jp z, Route22SetDefaultScript
+ xor a
+ ld [wIsTrainerBattle], a
ld a, [wSpritePlayerStateData1FacingDirection]
and a ; cp SPRITE_FACING_DOWN
...
and further down to add:
Route22Rival2AfterBattleScript:
ld a, [wIsInBattle]
cp $ff
jp z, Route22SetDefaultScript
+ xor a
+ ld [wIsTrainerBattle], a
ld a, ROUTE22_RIVAL2
ldh [hSpriteIndex], a
...
And lastly for Route 22,
Route22GetRivalTrainerNoByStarterScript:
ld a, [wRivalStarter]
ld b, a
.next_trainer_no
ld a, [hli]
cp b
jr z, .got_trainer_no
inc hl
jr .next_trainer_no
.got_trainer_no
ld a, [hl]
ld [wTrainerNo], a
+ ld a, 1
+ ld [wIsTrainerBattle], a
ret
Afterwards, modify scripts/CeruleanCity.asm:
...
.Charmander
ld a, $9
.done
ld [wTrainerNo], a
+ ld a, 1
+ ld [wIsTrainerBattle], a
xor a
ldh [hJoyHeld], a
call CeruleanCityFaceRivalScript
ld a, SCRIPT_CERULEANCITY_RIVAL_DEFEATED
ld [wCeruleanCityCurScript], a
ret
CeruleanCityRivalDefeatedScript:
ld a, [wIsInBattle]
cp $ff
jp z, CeruleanCityClearScripts
+ xor a
+ ld [wIsTrainerBattle], a
call CeruleanCityFaceRivalScript
ld a, D_RIGHT | D_LEFT | D_UP | D_DOWN
...
And scripts/SSAnne2F.asm:
...
.Charmander
ld a, $3
.done
ld [wTrainerNo], a
+ ld a, 1
+ ld [wIsTrainerBattle], a
call SSAnne2FSetFacingDirectionScript
ld a, SCRIPT_SSANNE2F_RIVAL_AFTER_BATTLE
ld [wSSAnne2FCurScript], a
ret
SSAnne2FRivalAfterBattleScript:
ld a, [wIsInBattle]
cp $ff
jp z, SSAnne2FResetScripts
+ xor a
+ ld [wIsTrainerBattle], a
call SSAnne2FSetFacingDirectionScript
...
And then scripts/PokemonTower2F.asm:
...
PokemonTower2FDefeatedRivalScript:
ld a, [wIsInBattle]
cp $ff
jp z, PokemonTower2FResetRivalEncounter
+ xor a
+ ld [wIsTrainerBattle], a
ld a, D_RIGHT | D_LEFT | D_UP | D_DOWN
ld [wJoyIgnore], a
...
And down a bit further:
...
.Charmander
ld a, $6
.done
ld [wTrainerNo], a
+ ld a, 1
+ ld [wIsTrainerBattle], a
ld a, SCRIPT_POKEMONTOWER2F_DEFEATED_RIVAL
ld [wPokemonTower2FCurScript], a
...
And also scripts/SilphCo7F.asm:
...
.set_trainer_no
ld [wTrainerNo], a
+ ld a, 1
+ ld [wIsTrainerBattle], a
ld a, SCRIPT_SILPHCO7F_RIVAL_AFTER_BATTLE
jp SilphCo7FSetCurScript
SilphCo7FRivalAfterBattleScript:
ld a, [wIsInBattle]
cp $ff
jp z, SilphCo7FSetDefaultScript
+ xor a
+ ld [wIsTrainerBattle], a
ld a, D_RIGHT | D_LEFT | D_UP | D_DOWN
...
And finally scripts/ChampionsRoom.asm:
...
.saveTrainerId
ld [wTrainerNo], a
+ ld a, 1
+ ld [wIsTrainerBattle], a
xor a
ldh [hJoyHeld], a
ld a, $3
ld [wChampionsRoomCurScript], a
ret
ChampionsRoomRivalDefeatedScript:
ld a, [wIsInBattle]
cp $ff
jp z, ResetRivalScript
+ xor a
+ ld [wIsTrainerBattle], a
call UpdateSprites
...
We first define the constant OW_POKEMON
in constants/map_object_constants.asm
...
DEF RIGHT EQU $D3
DEF NONE EQU $FF
+DEF OW_POKEMON EQU $80
DEF BOULDER_MOVEMENT_BYTE_2 EQU $10
Note that the hexadecimal number, $80
, is %10000000
in binary. Now we are going to append | OW_POKEMON
in the object files for the overworld Pokemon: Voltorb, Electrode, Zapdos, Articuno, Moltres, and Mewtwo. So say for Articuno, open up, data/maps/objects/SeafoamIslandsB4F.asm and make the change,
...
def_object_events
object_event 4, 15, SPRITE_BOULDER, STAY, NONE, 1 ; person
object_event 5, 15, SPRITE_BOULDER, STAY, NONE, 2 ; person
- object_event 6, 1, SPRITE_BIRD, STAY, DOWN, 3, ARTICUNO, 50
+ object_event 6, 1, SPRITE_BIRD, STAY, DOWN, 3, ARTICUNO, 50 | OW_POKEMON
def_warps_to SEAFOAM_ISLANDS_B4F
Do this for all the other overworld Pokemon except for Snorlax. You can find these overworld Pokemon in: data/maps/objects/PowerPlant.asm, data/maps/objects/VictoryRoad2F.asm, and data/maps/objects/CeruleanCaveB1F.asm.
To understand what is happening, the 50
that is appearing after ARTICUNO
is the level of the Pokemon. We've replaced it with 50 | OW_POKEMON
which is a binary OR operation. Note that 50
is equal to %00110010
in binary. Thus we have that %00110010 | %10000000 = %10110010
. This would mean that the Pokemon's level is 178
. This is okay, because OW_POKEMON
is just used as a flag to signify a Pokemon battle rather than a trainer battle. In particular, see the changes made to EngageMapTrainer
in home/trainers.asm
. We check if the last bit is flagged, if it is we know it's a Pokemon fight and perform a bitwise AND operation with $7F
which is %01111111
in binary. This results in the level of the Pokemon being set back to the original value, 50
in our example.
Lastly, we need to change the scripts for the Snorlax encounters. Snorlax is an overworld Pokemon but it is not treated the same way as say Voltorb in the script files. Make these changes to scripts/Route12.asm:
...
ld [wCurOpponent], a
ld a, 30
ld [wCurEnemyLVL], a
+ xor a
+ ld [wIsTrainerBattle], a
ld a, HS_ROUTE_12_SNORLAX
ld [wMissableObjectIndex], a
...
And scripts/Route16.asm:
...
ld [wCurOpponent], a
ld a, 30
ld [wCurEnemyLVL], a
+ xor a
+ ld [wIsTrainerBattle], a
ld a, HS_ROUTE_16_SNORLAX
ld [wMissableObjectIndex], a
...