Skip to content

Trainers are not Pokemon

Enrip edited this page Aug 21, 2024 · 12 revisions

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.

Define new bytes in WRAM

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
...

Modify the engine

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
...

Modify the Scripts for Rival

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.

Oak's Lab

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
...

Route 22

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

Cerulean City

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
...

S.S. Anne

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
...

Pokemon Tower

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
...

Silph Co.

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
...

Champion's Room

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
...

Handle Overworld Pokemon

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.

Modify the Scripts for Snorlax

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
...
Clone this wiki locally