Skip to content

Trainers are not Pokemon

Xillicis edited this page Jul 24, 2023 · 31 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 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.

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

Modify the Scripts for Rival

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

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