Add a new Unown form - pret/pokecrystal GitHub Wiki

This tutorial is for how to add a new Unown form. As an example, we'll add the ! and ? forms introduced in Gen 3.

Contents

  1. Define a Unown form constant
  2. Design its sprites and animation
  3. Include and point to the sprite and animation data
  4. Change how DVs determine forms to make them all available
  5. Make room for it in the Pokédex WRAM
  6. Create a Unown font character for the new form
  7. Define its Unown Mode word
  8. Update Unown Mode to make room for the new form
  9. Allow the new form to be unlocked in the wild
  10. Correct the Research Center computer
  11. Fix bank overflow errors

1. Define a Unown form constant

Edit constants/pokemon_constants.asm:

 ; Unown forms
 ; indexes for:
 ; - UnownWords (see data/pokemon/unown_words.asm)
 ; - UnownPicPointers (see data/pokemon/unown_pic_pointers.asm)
 ; - UnownAnimationPointers (see gfx/pokemon/unown_anim_pointers.asm)
 ; - UnownAnimationIdlePointers (see gfx/pokemon/unown_idle_pointers.asm)
 ; - UnownBitmasksPointers (see gfx/pokemon/unown_bitmask_pointers.asm)
 ; - UnownFramesPointers (see gfx/pokemon/unown_frame_pointers.asm)
 	const_def 1
 	const UNOWN_A ;  1
 	...
 	const UNOWN_Z ; 26
+	const UNOWN_EXCLAMATION
+	const UNOWN_QUESTION
 NUM_UNOWN EQU const_value + -1 ; 26

2. Design its sprites and animation

Create gfx/pokemon/unown_exclamation/front.png:

gfx/pokemon/unown_exclamation/front.png

And gfx/pokemon/unown_exclamation/back.png:

gfx/pokemon/unown_exclamation/back.png

Then create gfx/pokemon/unown_question/front.png:

gfx/pokemon/unown_question/front.png

And gfx/pokemon/unown_question/back.png:

gfx/pokemon/unown_question/back.png

front.png is a vertical strip of unique animation frames. Frames are all the same size, 40x40 pixels, since that's the size of all the other Unown forms. back.png is always 48x48. Both sprites have to use the same four colors: white, black, and the same two hues as every other Unown.

Now create gfx/pokemon/unown_exclamation/anim.asm:

+	frame 0, 09
+	frame 1, 09
+	frame 2, 09
+	frame 3, 05
+	frame 2, 05
+	frame 4, 05
+	frame 1, 05
+	frame 1, 05
+	endanim

And gfx/pokemon/unown_exclamation/anim_idle.asm:

+	frame 0, 18
+	setrepeat 2
+	frame 5, 05
+	frame 0, 05
+	dorepeat 2
+	endanim

Then create gfx/pokemon/unown_question/anim.asm:

+	frame 0, 09
+	setrepeat 3
+	frame 1, 05
+	frame 2, 05
+	frame 1, 05
+	frame 0, 05
+	dorepeat 2
+	endanim

And finally gfx/pokemon/unown_question/anim_idle.asm:

+	frame 0, 13
+	setrepeat 2
+	frame 3, 05
+	frame 0, 05
+	dorepeat 2
+	endanim

anim.asm defines the main sprite animation sequence; anim_idle.asm defines an extra one that gets played in some contexts (notably not when a Pokémon is encountered in battle). A full description of the animation script commands is at docs/pic_animations.md.

These animations were designed by SCMidna. But you're more likely to have a single static front sprite than a whole animated sequence of frames. In that case, you can save the one sprite as front.png (so it will be a single square frame, not a vertical strip of them), and just put endanim as the full contents of anim.asm and anim_idle.asm.

When you make the ROM, a number of sprite-related files will be automatically generated for you. For more information on that, see the tutorial for how to add a new Pokémon.

3. Include and point to the sprite and animation data

Edit data/pokemon/unown_pic_pointers.asm:

 UnownPicPointers::
 ; entries correspond to Unown letters, two apiece
 	dba_pic UnownAFrontpic
 	dba_pic UnownABackpic
 	...
 	dba_pic UnownZFrontpic
 	dba_pic UnownZBackpic
+	dba_pic UnownExclamationFrontpic
+	dba_pic UnownExclamationBackpic
+	dba_pic UnownQuestionFrontpic
+	dba_pic UnownQuestionBackpic

We have to use dba_pic here instead of a standard dba—declaring the bank and address of each label—because of this design flaw. I strongly recommend removing the whole FixPicBank routine from engine/gfx/load_pics.asm, including all four calls to it in that file, and just using dba here; then you'll be able to INCBIN sprites in arbitrary banks.

Edit gfx/pics.asm:

 SECTION "Pics 19", ROMX

-; Seems to be an accidental copy of the previous bank
-
-INCBIN "gfx/pokemon/spinarak/back.2bpp.lz"
-...
-INCBIN "gfx/pokemon/unown_r/back.2bpp.lz"
+UnownExclamationFrontpic: INCBIN "gfx/pokemon/unown_exclamation/front.animated.2bpp.lz"
+UnownExclamationBackpic:  INCBIN "gfx/pokemon/unown_exclamation/back.2bpp.lz"
+UnownQuestionFrontpic: INCBIN "gfx/pokemon/unown_question/front.animated.2bpp.lz"
+UnownQuestionBackpic:  INCBIN "gfx/pokemon/unown_question/back.2bpp.lz"

(If you don't fix the dba_pic design flaw, you'll have to put your sprites in the "Pics N" sections, which are compatible with dba_pic. "Pics 19" isn't used for anything useful—all its contents are unused duplicates of "Pics 18"—and it has a whole bank to itself, so it's the easiest place to start adding new sprites. (The other sections, includng "Pics 20" through "Pics 24", have limited remaining space since they already contain some sprites and/or share their banks with other sections.) But if you have a lot of new sprites to add, you risk overflowing the banks, and it's hard to fit sprites within fixed bank limits. By using just dba, you can create new sections with a few sprites each, that will automatically be placed wherever they can fit in the ROM.)

Anyway, edit gfx/pokemon/unown_anim_pointers.asm:

 UnownAnimationPointers:
 	dw UnownAAnimation
 	...
 	dw UnownZAnimation
+	dw UnownExclamationAnimation
+	dw UnownQuestionAnimation

Edit gfx/pokemon/unown_anims.asm:

 UnownAnimations: ; used only for BANK(UnownAnimations)

 UnownAAnimation: INCLUDE "gfx/pokemon/unown_a/anim.asm"
 ...
 UnownZAnimation: INCLUDE "gfx/pokemon/unown_z/anim.asm"
+UnownExclamationAnimation: INCLUDE "gfx/pokemon/unown_exclamation/anim.asm"
+UnownQuestionAnimation: INCLUDE "gfx/pokemon/unown_question/anim.asm"

Edit gfx/pokemon/unown_idle_pointers.asm:

 UnownAnimationIdlePointers:
 	dw UnownAAnimationIdle
 	...
 	dw UnownZAnimationIdle
+	dw UnownExclamationAnimationIdle
+	dw UnownQuestionAnimationIdle

Edit gfx/pokemon/unown_idles.asm:

 UnownAAnimationIdle: INCLUDE "gfx/pokemon/unown_a/anim_idle.asm"
 ...
 UnownZAnimationIdle: INCLUDE "gfx/pokemon/unown_z/anim_idle.asm"
+UnownExclamationAnimationIdle: INCLUDE "gfx/pokemon/unown_exclamation/anim_idle.asm"
+UnownQuestionAnimationIdle: INCLUDE "gfx/pokemon/unown_question/anim_idle.asm"

Edit gfx/pokemon/unown_bitmask_pointers.asm:

 UnownBitmasksPointers:
 	dw UnownABitmasks
 	...
 	dw UnownZBitmasks
+	dw UnownExclamationBitmasks
+	dw UnownQuestionBitmasks

Edit gfx/pokemon/unown_bitmasks.asm:

 UnownABitmasks: INCLUDE "gfx/pokemon/unown_a/bitmask.asm"
 ...
 UnownZBitmasks: INCLUDE "gfx/pokemon/unown_z/bitmask.asm"
+UnownExclamationBitmasks: INCLUDE "gfx/pokemon/unown_exclamation/bitmask.asm"
+UnownQuestionBitmasks: INCLUDE "gfx/pokemon/unown_question/bitmask.asm"

Edit gfx/pokemon/unown_frame_pointers.asm:

 UnownFramesPointers:
 	dw UnownAFrames
 	...
 	dw UnownZFrames
+	dw UnownExclamationFrames
+	dw UnownQuestionFrames

Finally, edit gfx/pokemon/unown_frames.asm:

 UnownsFrames: ; used only for BANK(UnownsFrames)

 UnownAFrames: INCLUDE "gfx/pokemon/unown_a/frames.asm"
 ...
 UnownZFrames: INCLUDE "gfx/pokemon/unown_z/frames.asm"
+UnownExclamationFrames: INCLUDE "gfx/pokemon/unown_exclamation/frames.asm"
+UnownQuestionFrames: INCLUDE "gfx/pokemon/unown_question/frames.asm"

4. Change how DVs determine forms to make them all available

Edit engine/gfx/load_pics.asm:

 GetUnownLetter:
 ; Return Unown letter in wUnownLetter based on DVs at hl

 ; Take the middle 2 bits of each DV and place them in order:
 ;	atk  def  spd  spc
 ;	.ww..xx.  .yy..zz.

 	; atk
 	ld a, [hl]
 	and %01100000
 	sla a
 	ld b, a
 	; def
 	ld a, [hli]
 	and %00000110
 	swap a
 	srl a
 	or b
 	ld b, a

 	; spd
 	ld a, [hl]
 	and %01100000
 	swap a
 	sla a
 	or b
 	ld b, a
 	; spc
 	ld a, [hl]
 	and %00000110
 	srl a
 	or b

-; Divide by 10 to get 0-25
+; Divide by 9 to get 0-28
 	ldh [hDividend + 3], a
 	xor a
 	ldh [hDividend], a
 	ldh [hDividend + 1], a
 	ldh [hDividend + 2], a
-	ld a, $ff / NUM_UNOWN + 1
+	ld a, 9
 	ldh [hDivisor], a
 	ld b, 4
 	call Divide

-; Increment to get 1-26
+; Increment to get 1-29
 	ldh a, [hQuotient + 3]
 	inc a
+; The valid range is 1-28, so use UNOWN_E (5) instead of 29
+	cp NUM_UNOWN + 1
+	jr c, .valid
+	ld a, UNOWN_E
+.valid
 	ld [wUnownLetter], a
 	ret

The comments in GetUnownLetter already describe how it works; as you can see, we've changed it to allow 28 possible values instead of 29. Before, Unown A to Y each had a 10/256 chance of appearing, and Unown Z only had a 6/256 chance (assuming wild DVs are perfectly random). Now, Unown A to Z, as well as ! and ?, all have a 9/256 chance of appearing—except for Unown E, which has a 13/256 chance because the routine defaults to it for the invalid 29th Unown.

Of course, the choice to give Unown E a boost is arbitrary. I picked it because E is the most common English letter; you could pick a different one, or try distributing the invalid cases more fairly to give four different forms a 10/256 chance; or completely revamp how forms are determined. The important thing is that every form needs to be available, and invalid forms should be impossible.

5. Make room for it in the Pokédex WRAM

Edit wram.asm:

-	ds 22
+	ds 20

 wPokedexCaught:: flag_array NUM_POKEMON ; de99
 wEndPokedexCaught::

 wPokedexSeen:: flag_array NUM_POKEMON ; deb9
 wEndPokedexSeen::

 wUnownDex:: ds NUM_UNOWN ; ded9
 wUnlockedUnowns:: db ; def3
 wFirstUnownSeen:: db

This WRAM bank is very close to being full. Since wUnownDex has grown by 2 bytes, we need to remove 2 bytes elsewhere. Luckily there are 22 unused bytes nearby.

6. Create a Unown font character for the new form

Edit gfx/font/unown_font.png:

gfx/font/unown_font.png

We've added the ! and ? characters after Z and before . This font gets used by the Unown Mode for the Pokédex.

7. Define its Unown Mode word

Edit data/pokemon/unown_words.asm:

 UnownWords:
 ; entries correspond to UNOWN_* form constants
 	dw UnownWordA
 	...
 	dw UnownWordZ
+	dw UnownWordExclamation
+	dw UnownWordQuestion

 UnownWordA: unownword "ANGRY"
 ...
 UnownWordZ: unownword "ZOOM"
+UnownWordExclamation: unownword "(((((" ; "!!!!!" since "Z" + 1 == "("
+UnownWordQuestion: unownword ")))))" ; "?????" since "Z" + 2 == ")"

The valid characters here correspond to the ones in gfx/font/unown_font.png: A to Z, plus the new ! and ?. You could also use ":" ("Z" + 3) for , but that's meant to be the cursor, not for words.

"!!!!!" and "?????" are the words from the Unown Report in HG/SS.

8. Update Unown Mode to make room for the new form

This is the default Unown Mode Pokédex:

Screenshot

It has room to add one more Unown form in the bottom-right, but for two, we'll also have to widen it a little.

Edit engine/pokedex/pokedex.asm:

 Pokedex_UnownModePlaceCursor:
 	ld a, [wDexCurUnownIndex]
-	ld c, $5a ; diamond cursor
+	ld c, $5c ; diamond cursor

...

 Pokedex_DrawUnownModeBG:
 	call Pokedex_FillBackgroundColor2
 	hlcoord 2, 1
-	lb bc, 10, 13
+	lb bc, 10, 14
 	call Pokedex_PlaceBorder
 	hlcoord 2, 14
-	lb bc, 1, 13
+	lb bc, 1, 14
 	call Pokedex_PlaceBorder
 	hlcoord 2, 15
 	ld [hl], $3d
-	hlcoord 16, 15
+	hlcoord 17, 15
 	ld [hl], $3e
 	hlcoord 6, 5
 	call Pokedex_PlaceFrontpicAtHL
 	ld de, 0
 	ld b, 0
-	ld c, 26
+	ld c, NUM_UNOWN
 .loop
 	ld hl, wUnownDex
 	add hl, de
 	ld a, [hl]
 	and a
 	jr z, .done
 	push af
 	ld hl, UnownModeLetterAndCursorCoords
 rept 4
 	add hl, de
 endr
 	ld a, [hli]
 	ld h, [hl]
 	ld l, a
 	pop af
 	add $40 - 1 ; Unown A
 	ld [hl], a
 	inc de
 	inc b
 	dec c
 	jr nz, .loop
 .done
 	ld a, b
 	ld [wDexUnownCount], a
 	ret

 UnownModeLetterAndCursorCoords:
 ; entries correspond to Unown forms
 ;           letter, cursor
 	dwcoord   4,11,   3,11 ; A
 	dwcoord   4,10,   3,10 ; B
 	dwcoord   4, 9,   3, 9 ; C
 	dwcoord   4, 8,   3, 8 ; D
 	dwcoord   4, 7,   3, 7 ; E
 	dwcoord   4, 6,   3, 6 ; F
 	dwcoord   4, 5,   3, 5 ; G
 	dwcoord   4, 4,   3, 4 ; H
 	dwcoord   4, 3,   3, 2 ; I
 	dwcoord   5, 3,   5, 2 ; J
 	dwcoord   6, 3,   6, 2 ; K
 	dwcoord   7, 3,   7, 2 ; L
 	dwcoord   8, 3,   8, 2 ; M
 	dwcoord   9, 3,   9, 2 ; N
 	dwcoord  10, 3,  10, 2 ; O
 	dwcoord  11, 3,  11, 2 ; P
 	dwcoord  12, 3,  12, 2 ; Q
 	dwcoord  13, 3,  13, 2 ; R
-	dwcoord  14, 3,  15, 2 ; S
-	dwcoord  14, 4,  15, 4 ; T
-	dwcoord  14, 5,  15, 5 ; U
-	dwcoord  14, 6,  15, 6 ; V
-	dwcoord  14, 7,  15, 7 ; W
-	dwcoord  14, 8,  15, 8 ; X
-	dwcoord  14, 9,  15, 9 ; Y
-	dwcoord  14,10,  15,10 ; Z
+	dwcoord  14, 3,  14, 2 ; S
+	dwcoord  15, 3,  16, 2 ; T
+	dwcoord  15, 4,  16, 4 ; U
+	dwcoord  15, 5,  16, 5 ; V
+	dwcoord  15, 6,  16, 6 ; W
+	dwcoord  15, 7,  16, 7 ; X
+	dwcoord  15, 8,  16, 8 ; Y
+	dwcoord  15, 9,  16, 9 ; Z
+	dwcoord  15,10,  16,10 ; !
+	dwcoord  15,11,  16,11 ; ?

...

 Pokedex_LoadUnownFont:
 	ld a, BANK(sScratch)
 	call OpenSRAM
 	ld hl, UnownFont
 	ld de, sScratch + $188
 	ld bc, 39 tiles
 	ld a, BANK(UnownFont)
 	call FarCopyBytes
 	ld hl, sScratch + $188
-	ld bc, 27 tiles
+	ld bc, (NUM_UNOWN + 1) tiles
 	call Pokedex_InvertTiles
 	ld de, sScratch + $188
 	ld hl, vTiles2 tile $40
-	lb bc, BANK(Pokedex_LoadUnownFont), 27
+	lb bc, BANK(Pokedex_LoadUnownFont), NUM_UNOWN + 1
 	call Request2bpp
 	call CloseSRAM
 	ret

First of all, notice that we had to add 2 to the value of the "diamond cursor" tile because we added two characters in front of it in gfx/font/unown_font.png.

As for Pokedex_DrawUnownModeBG, our edits do three things:

  • one, widen the interface;
  • two, define coordinates for the letter and cursor at new positions for ! and ? (and adjust the positions for S to Z to accomodate the wider interface);
  • three, change some hard-coded constants to depend on NUM_UNOWN (this was already done in pokecrystal as of July 19, 2018).

9. Allow the new form to be unlocked in the wild

There are four chambers in the Ruins of Alph with puzzles of Kabuto, Omanyte, Aerodactyl, and Ho-Oh; solving each one unlocks a set of more Unown forms to be available in the wild. You could simply add the new forms to one of their four sets, but this step will show you how to add a fifth set.

Edit constants/engine_flags.asm:

 ; wUnlockedUnowns
 	const ENGINE_UNLOCKED_UNOWNS_A_TO_K
 	const ENGINE_UNLOCKED_UNOWNS_L_TO_R
 	const ENGINE_UNLOCKED_UNOWNS_S_TO_W
 	const ENGINE_UNLOCKED_UNOWNS_X_TO_Z
-	const ENGINE_UNLOCKED_UNOWNS_UNUSED_4
+	const ENGINE_UNLOCKED_UNOWNS_EXCLAMATION_QUESTION
 	const ENGINE_UNLOCKED_UNOWNS_UNUSED_5 ; 30
 	const ENGINE_UNLOCKED_UNOWNS_UNUSED_6
 	const ENGINE_UNLOCKED_UNOWNS_UNUSED_7

Edit data/engine_flags.asm:

 	; unown sets (see data/wild/unlocked_unowns.asm)
 	engine_flag wUnlockedUnowns, 0 ; A-K
 	engine_flag wUnlockedUnowns, 1 ; L-R
 	engine_flag wUnlockedUnowns, 2 ; S-W
 	engine_flag wUnlockedUnowns, 3 ; X-Z
-	engine_flag wUnlockedUnowns, 4 ; unused
+	engine_flag wUnlockedUnowns, 4 ; !-?
 	engine_flag wUnlockedUnowns, 5 ; unused ; $30
 	engine_flag wUnlockedUnowns, 6 ; unused
 	engine_flag wUnlockedUnowns, 7 ; unused

Edit data/wild/unlocked_unowns.asm:

 UnlockedUnownLetterSets:
 ; entries correspond to wUnlockedUnowns bits
 	dw .Set_A_K ; ENGINE_UNLOCKED_UNOWNS_A_TO_K
 	dw .Set_L_R ; ENGINE_UNLOCKED_UNOWNS_L_TO_R
 	dw .Set_S_W ; ENGINE_UNLOCKED_UNOWNS_S_TO_W
 	dw .Set_X_Z ; ENGINE_UNLOCKED_UNOWNS_X_TO_Z
+	dw .Set_Exclamation_Question ; ENGINE_UNLOCKED_UNOWNS_EXCLAMATION_QUESTION
 .End

 .Set_A_K:
 	unown_set A, B, C, D, E, F, G, H, I, J, K
 .Set_L_R:
 	unown_set L, M, N, O, P, Q, R
 .Set_S_W:
 	unown_set S, T, U, V, W
 .Set_X_Z:
 	unown_set X, Y, Z
+.Set_Exclamation_Question:
+	unown_set EXCLAMATION, QUESTION

Finally, edit maps/RuinsOfAlphInnerChamber.asm:

 RuinsOfAlphInnerChamberStatue:
+	checkflag ENGINE_UNLOCKED_UNOWNS_EXCLAMATION_QUESTION
+	iftrue .already_unlocked
+	readvar VAR_UNOWNCOUNT
+	ifless 26, .dont_unlock
+	opentext
+	writetext RuinsOfAlphInnerChamberStatueText
+	waitbutton
+	writetext RuinsOfAlphInnerChamberStatueUnlockText
+	waitbutton
+	closetext
+	pause 30
+	earthquake 30
+	showemote EMOTE_SHOCK, PLAYER, 20
+	pause 30
+	playsound SFX_STRENGTH
+	earthquake 50
+	setflag ENGINE_UNLOCKED_UNOWNS_EXCLAMATION_QUESTION
+	jumptext RuinsOfAlphStrangePresenceText
+
+.already_unlocked
+.dont_unlock
 	jumptext RuinsOfAlphInnerChamberStatueText

 RuinsOfAlphStrangePresenceText:
 	text "There is a strange"
 	line "presence here…"
 	done

 ...

 RuinsOfAlphInnerChamberStatueText:
 	text "It's a replica of"
 	line "an ancient #-"
 	cont "MON."
 	done
+
+RuinsOfAlphInnerChamberStatueUnlockText:
+	text "…The statue is"
+	line "shaking!"
+	done

The simple method would be to just edit data/wild/unlocked_unowns.asm and add EXCLAMATION and QUESTION to one of the four existing sets. This demonstrates which other files need editing to add a fifth set.

We've also invented an event in the Ruins of Alph: if you talk to one of the statues after catching 26 different Unown, there will be an earthquake and the final set will be unlocked. (I copied the earthquake code from the wall-opening script in maps/RuinsOfAlphKabutoChamber.asm.)

10. Correct the Research Center computer

Edit maps/RuinsOfAlphResearchCenter.asm:

 RuinsOfAlphResearchCenterComputerText_GotAllUnown:
 	text "Mystery #MON"
 	line "Name: UNOWN"

-	para "A total of 26"
+	para "A total of 28"
 	line "kinds found."
 	done

11. Fix bank overflow errors

We're done adding the Unown forms, but make won't compile the ROM:

error: Unable to place 'Pics 2' (ROMX section) at $40A8 in bank $49

But we didn't change anything in "Pics 2", so why is this happening?

As defined in pokecrystal.link, the "Unown Pic Pointers" and "Pics 2" sections are both in bank $49:

ROMX $49
	org $4000
	"Unown Pic Pointers"
	"Pics 2"

It turns out that adding the new dba_pics to the UnownPicPointers table was a little too much data for that bank. To fix this, we'll need to move some data out of bank $49. "Unown Pic Pointers" has to be as large as it is, but "Pics 2" is an arbitrary set of sprites, so we can move one of those.

Edit gfx/pics.asm again:

 SECTION "Pics 2", ROMX
 
 BlastoiseFrontpic:   INCBIN "gfx/pokemon/blastoise/front.animated.2bpp.lz"
 RapidashFrontpic:    INCBIN "gfx/pokemon/rapidash/front.animated.2bpp.lz"
 MeganiumFrontpic:    INCBIN "gfx/pokemon/meganium/front.animated.2bpp.lz"
 NidoqueenFrontpic:   INCBIN "gfx/pokemon/nidoqueen/front.animated.2bpp.lz"
 HitmonleeFrontpic:   INCBIN "gfx/pokemon/hitmonlee/front.animated.2bpp.lz"
 ScizorFrontpic:      INCBIN "gfx/pokemon/scizor/front.animated.2bpp.lz"
 BeedrillFrontpic:    INCBIN "gfx/pokemon/beedrill/front.animated.2bpp.lz"
 ArcanineFrontpic:    INCBIN "gfx/pokemon/arcanine/front.animated.2bpp.lz"
 TyranitarFrontpic:   INCBIN "gfx/pokemon/tyranitar/front.animated.2bpp.lz"
 MoltresFrontpic:     INCBIN "gfx/pokemon/moltres/front.animated.2bpp.lz"
 ZapdosFrontpic:      INCBIN "gfx/pokemon/zapdos/front.animated.2bpp.lz"
 ArbokFrontpic:       INCBIN "gfx/pokemon/arbok/front.animated.2bpp.lz"
 MewtwoFrontpic:      INCBIN "gfx/pokemon/mewtwo/front.animated.2bpp.lz"
 FearowFrontpic:      INCBIN "gfx/pokemon/fearow/front.animated.2bpp.lz"
 CharizardFrontpic:   INCBIN "gfx/pokemon/charizard/front.animated.2bpp.lz"
-QuilavaFrontpic:     INCBIN "gfx/pokemon/quilava/front.animated.2bpp.lz"
 
 ...
 
 SECTION "Pics 19", ROMX
 
 UnownExclamationFrontpic: INCBIN "gfx/pokemon/unown_exclamation/front.animated.2bpp.lz"
 UnownExclamationBackpic:  INCBIN "gfx/pokemon/unown_exclamation/back.2bpp.lz"
 UnownQuestionFrontpic: INCBIN "gfx/pokemon/unown_question/front.animated.2bpp.lz"
 UnownQuestionBackpic:  INCBIN "gfx/pokemon/unown_question/back.2bpp.lz"
+QuilavaFrontpic:     INCBIN "gfx/pokemon/quilava/front.animated.2bpp.lz"

Now we're done!

Screenshot

You can add even more than 28 Unown forms, but the Unown mode interface will need redesigning to accomodate them. For example, here's a version that fits 33 forms:

Screenshot

All you have to do is: