-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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 for the trainers if that ID is larger than 200. The purpose of this tutorial is to provide a workaround to avoid these weird glitches.
This tutorial is a modification of work done in this commit by JustRegularLuna. I 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 this change:
...
.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:
...
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"
Next is home/trainers.asm:
...
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
...
Finally, we need to modify all the scripts which handle the Rival fights. We'll just go down the list. Start with scripts/OaksLab.asm:
...
OaksLabScript11:
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
...
Then, scripts/Route22.asm:
...
jr .asm_50eda
.asm_50ee1
ld a, [hl]
ld [wTrainerNo], a
+ ld a, 1
+ ld [wIsTrainerBattle], a
ret
...
Head down a bit further and add:
...
Route22Script2:
ld a, [wIsInBattle]
cp $ff
jp z, Route22Script_50ece
+ xor a
+ ld [wIsTrainerBattle], a
ld a, [wSpritePlayerStateData1FacingDirection]
...
and further down to add:
...
Route22Script5:
ld a, [wIsInBattle]
cp $ff
jp z, Route22Script_50ece
+ xor a
+ ld [wIsTrainerBattle], a
ld a, $2
...
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 CeruleanCityScript_1955d
ld a, SCRIPT_CERULEANCITY_RIVAL_DEFEATED
ld [wCeruleanCityCurScript], a
ret
CeruleanCityRivalDefeatedScript:
ld a, [wIsInBattle]
cp $ff
jp z, CeruleanCityScript_1948c
+ xor a
+ ld [wIsTrainerBattle], a
call CeruleanCityScript_1955d
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
GaryScript3:
ld a, [wIsInBattle]
cp $ff
jp z, ResetGaryScript
+ 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
...