-
Notifications
You must be signed in to change notification settings - Fork 806
Color party menu icons by species
The tutorial to add a new party menu icon explains how to add a different icon for each Pokémon species, but the icons are all still red. This tutorial will explain how to color each species' icon from a set of eight palettes. (Coloring them uniquely, with the same palettes as their battle sprites, is beyond the scope of this tutorial.)
- Define the eight icon palettes
- Assign the palettes to Pokémon
- Allow sprite animations to have dynamic palette attributes
- Make party icons' sprite animation data use dynamic palettes
- Load the correct icon palettes for species and shininess
- Make the overworld Fly animation use the right palette
- Fix a bank overflow error
First we'll define the constants we need for our eight palettes. Let's edit constants/icon_constants.asm, adding them at the end of the file:
+; party menu icon palettes
+ const_def
+ const PAL_ICON_RED ; 0
+ const PAL_ICON_BLUE ; 1
+ const PAL_ICON_GREEN ; 2
+ const PAL_ICON_BROWN ; 3
+ const PAL_ICON_PINK ; 4
+ const PAL_ICON_GRAY ; 5
+ const PAL_ICON_TEAL ; 6
+ const PAL_ICON_PURPLE ; 7
Then edit gfx/stats/party_menu_ob.pal to add the respective colors; delete the entire content and replace it with this:
+ RGB 27,31,27, 31,19,10, 31,07,01, 00,00,00 ; red
+ RGB 27,31,27, 31,19,10, 10,09,31, 00,00,00 ; blue
+ RGB 27,31,27, 31,19,10, 07,23,03, 00,00,00 ; green
+ RGB 27,31,27, 31,19,10, 15,10,03, 00,00,00 ; brown
+ RGB 27,31,27, 31,19,10, 30,10,11, 00,00,00 ; pink
+ RGB 27,31,27, 31,19,10, 13,13,13, 00,00,00 ; gray
+ RGB 27,31,27, 31,19,10, 03,23,21, 00,00,00 ; teal
+ RGB 27,31,27, 31,19,10, 18,04,18, 00,00,00 ; purple
Finally, edit engine/gfx/color.asm:
InitPartyMenuOBPals:
ld hl, PartyMenuOBPals
ld de, wOBPals1
- ld bc, 2 palettes
+ ld bc, 8 palettes
ld a, BANK(wOBPals1)
call FarCopyWRAM
ret
These will be the eight possible colors for all 251 Pokémon (and Eggs). You can choose any colors you want, but the first two will have to be red and blue respectively, since the Fly map uses those for the player sprite at the same time as it shows the flying Pokémon. (InitPartyMenuOBPals
used to only load those first two, since they were all that was needed: Chris and the Pokémon were red, Kris was blue.)
(If you've followed the tutorial to edit the player colors, and/or the tutorial to add a third player choice, that's fine; just make sure that the possible player colors all correspond to identical Pokémon icon colors.)
Create data/pokemon/menu_icon_pals.asm:
+MACRO icon_pals
+ dn PAL_ICON_\1, PAL_ICON_\2
+ENDM
+
+MonMenuIconPals:
+ table_width 1, MonMenuIconPals
+ ; normal, shiny
+ icon_pals TEAL, GREEN ; BULBASAUR
+ icon_pals TEAL, GREEN ; IVYSAUR
+ icon_pals TEAL, GREEN ; VENUSAUR
+ icon_pals RED, BROWN ; CHARMANDER
+ icon_pals RED, BROWN ; CHARMELEON
+ icon_pals RED, PURPLE ; CHARIZARD
+ icon_pals BLUE, TEAL ; SQUIRTLE
+ icon_pals BLUE, TEAL ; WARTORTLE
+ icon_pals BLUE, TEAL ; BLASTOISE
+ ...
+ icon_pals GREEN, TEAL ; LARVITAR
+ icon_pals BLUE, PURPLE ; PUPITAR
+ icon_pals GREEN, BROWN ; TYRANITAR
+ icon_pals BLUE, TEAL ; LUGIA
+ icon_pals RED, BROWN ; HO_OH
+ icon_pals GREEN, PINK ; CELEBI
+ assert_table_length NUM_POKEMON
+
+ icon_pals RED, RED ; 252
+ icon_pals RED, BLUE ; EGG
+ icon_pals RED, RED ; 254
You may have noticed that this file is missing most of the Pokémon entries. You can either choose you own palettes, or use an existing file that has the complete list of palettes like this one: https://github.com/Rangi42/polishedcrystal/blob/master/data/pokemon/menu_icon_pals.asm
Then edit engine/gfx/mon_icons.asm:
INCLUDE "data/pokemon/menu_icons.asm"
+
+INCLUDE "data/pokemon/menu_icon_pals.asm"
INCLUDE "data/icon_pointers.asm"
INCLUDE "gfx/icons.asm"
This defines the normal and shiny color for each Pokémon. Note that EGG
(index 253, or $FD) also has an entry; if you give it a different shiny color (here BLUE
instead of RED
) you'll be able to tell shiny Eggs apart.
The sprite animation engine has certain data for Pokémon icons, Pokémon holding items, and Pokémon holding Mail. This data is accessed at certain points in the code, like the party menu and the Fly map, and is hard-coded to use PAL_OW_RED
. To change the color based on species, we need to achieve two sub-goals: get the right color corresponding to the current Pokémon species and apply that color to the sprite animation; and don't then force the animation to be red. Let's take those in reverse order; it's easier.
Edit engine/gfx/sprites.asm:
UpdateAnimFrame:
...
; fourth byte: attributes
; [de] = GetSpriteOAMAttr([hl])
+ ld a, [hl]
+ cp -1
+ jr z, .skip_attributes
call GetSpriteOAMAttr
ld [de], a
+.skip_attributes
inc hl
inc de
ld a, e
ld [wCurSpriteOAMAddr], a
cp LOW(wShadowOAMEnd)
jr nc, .reached_the_end
dec c
jr nz, .loop
pop bc
jr .done
Now we'll be able to load a species-appropriate palette index, and then load some sprite animation data; if that data uses −1 for the attribute value, it will just apply the already-loaded palette index instead. We'll see where that −1 value gets used in the next step.
First, edit constants/sprite_anim_constants.asm:
; SpriteAnimOAMData indexes (see data/sprite_anims/oam.asm)
const_def
const SPRITE_ANIM_OAMSET_RED_WALK_1 ; 00
const SPRITE_ANIM_OAMSET_RED_WALK_2 ; 01
...
const SPRITE_ANIM_OAMSET_PARTY_MON_WITH_MAIL_1 ; 3d
const SPRITE_ANIM_OAMSET_PARTY_MON_WITH_MAIL_2 ; 3e
const SPRITE_ANIM_OAMSET_PARTY_MON_WITH_ITEM_1 ; 3f
const SPRITE_ANIM_OAMSET_PARTY_MON_WITH_ITEM_2 ; 40
...
const SPRITE_ANIM_OAMSET_GAMEFREAK_LOGO_10 ; 8a
const SPRITE_ANIM_OAMSET_GAMEFREAK_LOGO_11 ; 8b
+ const SPRITE_ANIM_OAMSET_PARTY_MON_1 ; 8c
+ const SPRITE_ANIM_OAMSET_PARTY_MON_2 ; 8d
We're going to be updating the data soon for those SPRITE_ANIM_OAMSET_PARTY_MON_WITH_MAIL_*
and SPRITE_ANIM_OAMSET_PARTY_MON_WITH_ITEM_*
indexes; but the SPRITE_ANIM_OAMSET_RED_WALK_*
indexes are used for more things than just Pokémon, so we're creating new SPRITE_ANIM_OAMSET_PARTY_MON_*
indexes for plain party icons.
Edit data/sprite_anims/framesets.asm:
.Frameset_PartyMon:
- oamframe SPRITE_ANIM_OAMSET_RED_WALK_1, 8
- oamframe SPRITE_ANIM_OAMSET_RED_WALK_2, 8
+ oamframe SPRITE_ANIM_OAMSET_PARTY_MON_1, 8
+ oamframe SPRITE_ANIM_OAMSET_PARTY_MON_2, 8
oamrestart
.Frameset_PartyMonWithMail:
oamframe SPRITE_ANIM_OAMSET_PARTY_MON_WITH_MAIL_1, 8
oamframe SPRITE_ANIM_OAMSET_PARTY_MON_WITH_MAIL_2, 8
oamrestart
.Frameset_PartyMonWithItem:
oamframe SPRITE_ANIM_OAMSET_PARTY_MON_WITH_ITEM_1, 8
oamframe SPRITE_ANIM_OAMSET_PARTY_MON_WITH_ITEM_2, 8
oamrestart
.Frameset_PartyMonFast:
- oamframe SPRITE_ANIM_OAMSET_RED_WALK_1, 4
- oamframe SPRITE_ANIM_OAMSET_RED_WALK_2, 4
+ oamframe SPRITE_ANIM_OAMSET_PARTY_MON_1, 4
+ oamframe SPRITE_ANIM_OAMSET_PARTY_MON_2, 4
oamrestart
.Frameset_PartyMonWithMailFast:
oamframe SPRITE_ANIM_OAMSET_PARTY_MON_WITH_MAIL_1, 4
oamframe SPRITE_ANIM_OAMSET_PARTY_MON_WITH_MAIL_2, 4
oamrestart
.Frameset_PartyMonWithItemFast:
oamframe SPRITE_ANIM_OAMSET_PARTY_MON_WITH_ITEM_1, 4
oamframe SPRITE_ANIM_OAMSET_PARTY_MON_WITH_ITEM_2, 4
oamrestart
.Frameset_RedWalk:
oamframe SPRITE_ANIM_OAMSET_RED_WALK_1, 8
oamframe SPRITE_ANIM_OAMSET_RED_WALK_2, 8
oamframe SPRITE_ANIM_OAMSET_RED_WALK_1, 8
oamframe SPRITE_ANIM_OAMSET_RED_WALK_2, 8, OAM_X_FLIP
oamrestart
And edit data/sprite_anims/oam.asm:
SpriteAnimOAMData:
; entries correspond to SPRITE_ANIM_OAMSET_* constants
table_width 3, SpriteAnimOAMData
; vtile offset, data pointer
spriteanimoam $00, .OAMData_RedWalk ; SPRITE_ANIM_OAMSET_RED_WALK_1
spriteanimoam $04, .OAMData_RedWalk ; SPRITE_ANIM_OAMSET_RED_WALK_2
...
spriteanimoam $00, .OAMData_PartyMonWithMail1 ; SPRITE_ANIM_OAMSET_PARTY_MON_WITH_MAIL_1
spriteanimoam $00, .OAMData_PartyMonWithMail2 ; SPRITE_ANIM_OAMSET_PARTY_MON_WITH_MAIL_2
spriteanimoam $00, .OAMData_PartyMonWithItem1 ; SPRITE_ANIM_OAMSET_PARTY_MON_WITH_ITEM_1
spriteanimoam $00, .OAMData_PartyMonWithItem2 ; SPRITE_ANIM_OAMSET_PARTY_MON_WITH_ITEM_2
...
spriteanimoam $04, .OAMData_GameFreakLogo4_11 ; SPRITE_ANIM_OAMSET_GAMEFREAK_LOGO_10
spriteanimoam $00, .OAMData_GameFreakLogo4_11 ; SPRITE_ANIM_OAMSET_GAMEFREAK_LOGO_11
+ spriteanimoam $00, .OAMData_PartyMon ; SPRITE_ANIM_OAMSET_PARTY_MON_1
+ spriteanimoam $04, .OAMData_PartyMon ; SPRITE_ANIM_OAMSET_PARTY_MON_2
assert_table_length NUM_SPRITE_ANIM_OAMSETS
...
.OAMData_PartyMonWithMail1:
db 4
- dbsprite -1, -1, 0, 0, $00, PAL_OW_RED
- dbsprite 0, -1, 0, 0, $01, PAL_OW_RED
- dbsprite -1, 0, 0, 0, $08, PAL_OW_RED
- dbsprite 0, 0, 0, 0, $03, PAL_OW_RED
+ dbsprite -1, -1, 0, 0, $00, -1
+ dbsprite 0, -1, 0, 0, $01, -1
+ dbsprite -1, 0, 0, 0, $08, PAL_ICON_RED
+ dbsprite 0, 0, 0, 0, $03, -1
.OAMData_PartyMonWithMail2:
db 4
- dbsprite -1, -1, 0, 0, $04, PAL_OW_RED
- dbsprite 0, -1, 0, 0, $05, PAL_OW_RED
- dbsprite -1, 0, 0, 0, $08, PAL_OW_RED
- dbsprite 0, 0, 0, 0, $07, PAL_OW_RED
+ dbsprite -1, -1, 0, 0, $04, -1
+ dbsprite 0, -1, 0, 0, $05, -1
+ dbsprite -1, 0, 0, 0, $08, PAL_ICON_RED
+ dbsprite 0, 0, 0, 0, $07, -1
.OAMData_PartyMonWithItem1:
db 4
- dbsprite -1, -1, 0, 0, $00, PAL_OW_RED
- dbsprite 0, -1, 0, 0, $01, PAL_OW_RED
- dbsprite -1, 0, 0, 0, $09, PAL_OW_RED
- dbsprite 0, 0, 0, 0, $03, PAL_OW_RED
+ dbsprite -1, -1, 0, 0, $00, -1
+ dbsprite 0, -1, 0, 0, $01, -1
+ dbsprite -1, 0, 0, 0, $09, PAL_ICON_RED
+ dbsprite 0, 0, 0, 0, $03, -1
.OAMData_PartyMonWithItem2:
db 4
- dbsprite -1, -1, 0, 0, $04, PAL_OW_RED
- dbsprite 0, -1, 0, 0, $05, PAL_OW_RED
- dbsprite -1, 0, 0, 0, $09, PAL_OW_RED
- dbsprite 0, 0, 0, 0, $07, PAL_OW_RED
+ dbsprite -1, -1, 0, 0, $04, -1
+ dbsprite 0, -1, 0, 0, $05, -1
+ dbsprite -1, 0, 0, 0, $09, PAL_ICON_RED
+ dbsprite 0, 0, 0, 0, $07, -1
+
+.OAMData_PartyMon:
+ db 4
+ dbsprite -1, -1, 0, 0, $00, -1
+ dbsprite 0, -1, 0, 0, $01, -1
+ dbsprite -1, 0, 0, 0, $02, -1
+ dbsprite 0, 0, 0, 0, $03, -1
Now Pokémon icon sprites will use whatever palette index was already loaded, except for their held item or Mail graphics, which will use PAL_ICON_RED
. Of course, items and Mail could use different colors instead, but that's also out of the scope of the tutorial.
Edit engine/gfx/mon_icons.asm again:
LoadOverworldMonIcon:
ld a, e
call ReadMonMenuIcon
ld l, a
ld h, 0
add hl, hl
ld de, IconPointers
add hl, de
ld a, [hli]
ld e, a
ld d, [hl]
ld b, BANK(Icons)
ld c, 8
ret
+
+SetMenuMonIconColor:
+ push hl
+ push de
+ push bc
+ push af
+
+ ld a, [wTempIconSpecies]
+ ld [wCurPartySpecies], a
+ call GetMenuMonIconPalette
+ ld hl, wShadowOAMSprite00Attributes
+ jr _ApplyMenuMonIconColor
+
+SetMenuMonIconColor_NoShiny:
+ push hl
+ push de
+ push bc
+ push af
+
+ ld a, [wTempIconSpecies]
+ ld [wCurPartySpecies], a
+ and a
+ call GetMenuMonIconPalette_PredeterminedShininess
+ ld hl, wShadowOAMSprite00Attributes
+ jr _ApplyMenuMonIconColor
+
+LoadPartyMenuMonIconColors:
+ push hl
+ push de
+ push bc
+ push af
+
+ ld a, [wPartyCount]
+ sub c
+ ld [wCurPartyMon], a
+ ld e, a
+ ld d, 0
+
+ ld hl, wPartyMon1Item
+ call GetPartyLocation
+ ld a, [hl]
+ ld [wCurIconMonHasItemOrMail], a
+
+ ld hl, wPartySpecies
+ add hl, de
+ ld a, [hl]
+ ld [wCurPartySpecies], a
+ ld a, MON_DVS
+ call GetPartyParamLocation
+ call GetMenuMonIconPalette
+ ld hl, wShadowOAMSprite00Attributes
+ push af
+ ld a, [wCurPartyMon]
+ swap a
+ ld d, 0
+ ld e, a
+ add hl, de
+ pop af
+
+ ld de, 4
+ ld [hl], a ; top left
+ add hl, de
+ ld [hl], a ; top right
+ add hl, de
+ push hl
+ add hl, de
+ ld [hl], a ; bottom right
+ pop hl
+ ld d, a
+ ld a, [wCurIconMonHasItemOrMail]
+ and a
+ ld a, PAL_OW_RED ; item or mail color
+ jr nz, .ok
+ ld a, d
+.ok
+ ld [hl], a ; bottom left
+ jr _FinishMenuMonIconColor
+
+_ApplyMenuMonIconColor:
+ ld c, 4
+ ld de, 4
+.loop
+ ld [hl], a
+ add hl, de
+ dec c
+ jr nz, .loop
+ ; fallthrough
+_FinishMenuMonIconColor:
+ pop af
+ pop bc
+ pop de
+ pop hl
+ ret
+
+GetMenuMonIconPalette:
+ ld c, l
+ ld b, h
+ farcall CheckShininess
+GetMenuMonIconPalette_PredeterminedShininess:
+ push af
+ ld a, [wCurPartySpecies]
+ dec a
+ ld c, a
+ ld b, 0
+ ld hl, MonMenuIconPals
+ add hl, bc
+ ld e, [hl]
+ pop af
+ ld a, e
+ jr c, .shiny
+ swap a
+.shiny
+ and $f
+ ret
InitPartyMenuIcon:
+ call LoadPartyMenuMonIconColors
ld a, [wCurIconTile]
push af
ldh a, [hObjectStructIndex]
ld hl, wPartySpecies
ld e, a
ld d, 0
add hl, de
ld a, [hl]
call ReadMonMenuIcon
ld [wCurIcon], a
call GetMemIconGFX
...
NamingScreen_InitAnimatedMonIcon:
+ ld hl, wTempMonDVs
+ call SetMenuMonIconColor
ld a, [wTempIconSpecies]
call ReadMonMenuIcon
ld [wCurIcon], a
xor a
call GetIconGFX
depixel 4, 4, 4, 0
ld a, SPRITE_ANIM_INDEX_PARTY_MON
call _InitSpriteAnimStruct
ld hl, SPRITEANIMSTRUCT_ANIM_SEQ_ID
add hl, bc
ld [hl], SPRITE_ANIM_SEQ_NULL
ret
MoveList_InitAnimatedMonIcon:
+ ld a, MON_DVS
+ call GetPartyParamLocation
+ call SetMenuMonIconColor
ld a, [wTempIconSpecies]
call ReadMonMenuIcon
ld [wCurIcon], a
xor a
call GetIconGFX
ld d, 3 * 8 + 2 ; depixel 3, 4, 2, 4
ld e, 4 * 8 + 4
ld a, SPRITE_ANIM_INDEX_PARTY_MON
call _InitSpriteAnimStruct
ld hl, SPRITEANIMSTRUCT_ANIM_SEQ_ID
add hl, bc
ld [hl], SPRITE_ANIM_SEQ_NULL
ret
Trade_LoadMonIconGFX:
ld a, [wTempIconSpecies]
call ReadMonMenuIcon
ld [wCurIcon], a
ld a, $62
ld [wCurIconTile], a
call GetMemIconGFX
ret
GetSpeciesIcon:
; Load species icon into VRAM at tile a
push de
+ ld a, MON_DVS
+ call GetPartyParamLocation
+ call SetMenuMonIconColor
ld a, [wTempIconSpecies]
call ReadMonMenuIcon
ld [wCurIcon], a
pop de
ld a, e
call GetIconGFX
ret
FlyFunction_GetMonIcon:
push de
ld a, [wTempIconSpecies]
call ReadMonMenuIcon
ld [wCurIcon], a
pop de
ld a, e
call GetIcon_a
ret
Also we need to edit engine/menus/naming_screen.asm to load the appropiate DV values to wTempMonDVs
(which is used by NamingScreen_InitAnimatedMonIcon
in the previous file we edited), and this depends whether the Pokémon we receive/catch ends up in the player's party or in a box. For this, let's to go NamingScreen
:
NamingScreen:
...
.Pokemon:
ld a, [wCurPartySpecies]
ld [wTempIconSpecies], a
+ ; Is it a PartyMon or a BoxMon?
+ ld a, [wMonType]
+ and a
+ jr z, .party_mon
+
+ ld hl, sBoxMon1DVs
+ ld a, BANK(sBox)
+ call OpenSRAM
+ jr .start
+
+.party_mon
+ ld a, MON_DVS
+ call GetPartyParamLocation
+.start
+ ld de, wTempMonDVs
+ ld a, [hli]
+ ld [de], a
+ inc de
+ ld a, [hl]
+ ld [de], a
+ ld a, [wMonType]
+ cp BOXMON
+ call z, CloseSRAM
ld hl, LoadMenuMonIcon
ld a, BANK(LoadMenuMonIcon)
ld e, MONICON_NAMINGSCREEN
rst FarCall
...
And finally edit ram/wram.asm:
wHandlePlayerStep:: db
- ds 1
+wCurIconMonHasItemOrMail:: db
wPartyMenuActionText:: db
That code will load the correct icon colors on certain screens: the party menu, nickname screen, move list, and Fly map. Notably, the trade animation and overworld Fly animation do not load the new palettes. That's because those screens use some of the sprite palettes for their own elements (the trade tube, and other overworld sprites), so not all the possible species would have their correct colors available. There are different possible ways to overcome this, such as redesigning the trade and Fly animations, or converting some icon palettes on those screens, but that's left as an exercise for the reader. ;)
So far, the overworld fly animation still uses the OBJ 0 palette (the red one). An easy way to fix it is to override the colors of this palette.
We will make the palette change happen at the same time as the Pokémon sprite is loaded, which happens in FlyFunction_GetMonIcon
in engine/gfx/mon_icons.asm
FlyFunction_GetMonIcon:
push de
ld a, [wTempIconSpecies]
call ReadMonMenuIcon
ld [wCurIcon], a
pop de
ld a, e
call GetIcon_a
+ ; Edit the OBJ 0 palette so that the flying Pokémon has the right colors.
+ ld a, MON_DVS
+ call GetPartyParamLocation
+ call GetMenuMonIconPalette
+ add a
+ add a
+ add a
+ ld e, a
+ farcall SetFirstOBJPalette
ret
We're calling SetFirstOBJPalette
but it doesn't exist yet, so let's create and put it at the end of engine/gfx/color.asm:
FemalePokegearPals:
INCLUDE "gfx/pokegear/pokegear_f.pal"
+
+; Input: E must contain the offset of the selected palette from PartyMenuOBPals.
+SetFirstOBJPalette::
+ ld hl, PartyMenuOBPals
+ ld d, 0
+ add hl, de
+ ld de, wOBPals1
+ ld bc, 1 palettes
+ ld a, BANK(wOBPals1)
+ call FarCopyWRAM
+ ld a, TRUE
+ ldh [hCGBPalUpdate], a
+ jp ApplyPals
Now we need to reset the OBJ 0 palette to its red color after the player landed. To do this, edit engine/events/overworld.asm
.ReturnFromFly:
+ ld e, PAL_OW_RED
+ farcall SetFirstOBJPalette
farcall RespawnPlayer
Please note that this method is not compatible with "Make overworld sprites darker at night".
We're done implementing the new palettes, but probably we'll have this error when building the ROM:
ERROR: main.asm(324) -> engine/gfx/mon_icons.asm(556) -> gfx/icons.asm(40):
Section 'bank23' is too big (max size = 0x4000 bytes, reached 0x403F).
All that code and data we added has overflowed section "bank23", which layout.link tells us is in ROM bank $23. (Okay, duh, but not every section is named after its bank.) We need to move some content to a different bank.
It turns out that engine/gfx/mon_icons.asm INCLUDE
s gfx/icons.asm, which is just all the icon graphics. They're loaded by bank anyway, so they can freely be placed in any bank without causing errors.
Edit gfx/icons.asm:
+SECTION "Mon Icons", ROMX
+
Icons: ; used only for BANK(Icons)
NullIcon:
PoliwagIcon: INCBIN "gfx/icons/poliwag.2bpp"
JigglypuffIcon: INCBIN "gfx/icons/jigglypuff.2bpp"
DiglettIcon: INCBIN "gfx/icons/diglett.2bpp"
...
Now it builds correctly!
This is totally compatible with the tutorial to add a unique icon for each Pokémon; in that tutorial, we just split gfx/icons.asm into more than one section, and change how the graphics' bank is determined.