DV Checker - pret/pokered GitHub Wiki
Introduction
In the first generation of Pokemon games there is no in-game way to deduce what your Pokemon's DVs are. This tutorial aims to implement a "DV Checker" NPC that will read out the DVs of your LEAD Pokemon (see Conclusion section), thus giving the player some idea of how that individual compares to other members of its species. There will be sections of detailed explanations throughout; if you already know what DVs are and how they're stored (or if you simply don't care...), feel free to skip through to the code sections and Copy/Paste. There are annotations aplenty if you're interested.
Let's dive in!
Creating the DV Checker NPC
First, we need to choose the map in which our DV Checker will be located. Go to the \data\maps\objects folder and find a suitable map. This tutorial will place the DV Checker in the Celadon Department Store 1F, but you choose wherever you like!
First, open up \data\maps\objects\CeladonMart1F.asm, then add the following:
object_const_def
const_export CELADONMART1F_RECEPTIONIST
+ const_export CELADONMART1F_DV_CHECKER
+
CeladonMart1F_Object:
...
Next, find the def_object_events section in the same file make this addition:
NOTE: Make sure his position on both object lists is the same! In this example he is the second entry on both the former and following lists, placed immediately behind the Receptionist.
...
def_object_events
object_event 8, 3, SPRITE_LINK_RECEPTIONIST, STAY, DOWN, TEXT_CELADONMART1F_RECEPTIONIST
+ object_event 19, 5, SPRITE_GRAMPS, STAY, LEFT, TEXT_CELADONMART1F_DV_CHECKER ; choose a sprite from \constants\sprite_constants.asm
def_warps_to CELADON_MART_1F
NOTE: If you choose an outdoor map, make sure you select a DV Checker sprite that is on the sprite list for that area. See \data\maps\sprite_sets.asm for a list of eligible sprites for each outdoor map. Failure to do so will result in a DV Checker whose sprite is a garbled mess that is only vaguely human-shaped. It won't break the game, it just looks... weird.
Anyhow, the DV Checker NPC now exists; he'll be sitting in the Department Store Lobby on one of the benches on the right-hand side. However, before we get into the script, let's talk about why this DV checking process is so complicated...
(Skip the next couple of sections if you just want the code...)
Some Technical Stuff...
We'll start off with bits, bytes, and nybbles. What are they, and how are they related to DVs? First off:
- Bit: the smallest unit of data. A single bit can represent one of two values: 0 or 1
- Byte: a group of eight bits. All possible combinations of 8 bits mean a byte can represent one of 256 values: 0-255 (or 00000000 to 11111111)
- Nybble: a group of four bits. A nybble can represent one of 16 values: 0-15 (or 0000 to 1111)
From this we can see that each byte contains two nybbles. Within each byte, the first four bits are called the "high nybble", while the last four bits are called the "low nybble."
In the same vein, each group of bits has a "Most Significant Bit" (MSB, the left-most bit) and "Least Significant Bit" (LSB, the right-most bit). We won't worry about MSBs here... the LSB is what we're concerned with. The LSB alone determines whether the entire collection of bits (regardless of size) represents an odd or even number. This is critical information for calculating the HP DV, as we'll see shortly.
How DVs Are Stored
For each Pokemon, the game stores the information for four DVs: Attack, Defense, Speed, and Special. Each DV is composed of a 4-bit value ranging from 0-15 (technically, from 0000 to 1111).
Hey, that sounds an awful lot like a nybble, right?!
That's because that is indeed all they are. Each DV is a nybble of information, and they're stored together in pairs as a single byte. Attack and Defense make one pair, while Speed and Special create the other pair. These pairs are stored in neighboring WRAM addresses. Unless you've done some major overhauling of your RAM storage (in which case, if you're so smart why would you even need to read this tutorial???), your lead Pokemon's DVs can be found at WRAM addresses $D186 (Attack and Defense) and $D187 (Speed and Special). Worth noting is that Attack and Speed are the high nybbles of their respective pairs, while Defense and Special are the low nybbles.
But what about the HP DV value? Well, that one doesn't have its own explicit value that's stored anywhere. Rather, the game infers its value based on the parity (evenness or oddness) of the other 4 DVs. The HP DV is calculated as follows:
- Odd Attack DV = +8 to HP DV
- Odd Defense DV = +4 to HP DV
- Odd Speed DV = +2 to HP DV
- Odd Special DV = +1 to HP DV
So now we see the hurdles with checking individual DVs. For the Attack, Defense, Speed and Special DVs, we have no way to directly reference their values. We can read 1-byte addresses, sure, but to read nybbles requires us to jump through a few hoops first. If, for example, a Pokemon has an Attack DV of 11 ('B' in hex) and a Defense DV of 05, the value at $D186 would read "B5." If the game simply reads that, it will uselessly interpret the value as '181' (the decimal equivalent of 'B5'), for the same reason that we see 1105 and read "one thousand one hundred five" instead of "eleven and five." Our code MUST make the game see 'B' and '5' as two separate nybbles instead of one whole byte ('B5').
As for the HP DV, we must go through a TON of rigamarole to first determine parity for the other 4 DVs, and THEN we have to establish what to add and what not to add before we arrive at the final value to be displayed. Whew...
With all that said, let's now move on to the script...
Creating the DV Checker Script
Go to \scripts\CeladonMart1F.asm and insert the following sizeable chuck of code:
NOTE: Make sure your object_event scripts come BEFORE your bg_event scripts. Things will crash hard if you don't... which is weird, because the bg_events are listed first in the data\maps\objects files. But anyway...
...
CeladonMart1F_TextPointers:
def_text_pointers
dw_const CeladonMart1FReceptionistText, TEXT_CELADONMART1F_RECEPTIONIST
+ dw_const CeladonMart1FDVCheckerText, TEXT_CELADONMART1F_DV_CHECKER
dw_const CeladonMart1FDirectorySignText, TEXT_CELADONMART1F_DIRECTORY_SIGN
dw_const CeladonMart1FCurrentFloorSignText, TEXT_CELADONMART1F_CURRENT_FLOOR_SIGN
CeladonMart1FReceptionistText:
text_far _CeladonMart1FReceptionistText
text_end
+CeladonMart1FDVCheckerText:
+ text_asm
+ ld a, [wWhichPokemon] ; Loads last PartyMon name into register 'a'
+ ld bc, wPartyMon2 - wPartyMon1 ; Moves up through the party list
+ call AddNTimes ; Keeps moving until the top (LeadMon) slot is reached
+ ld hl, wPartyMonNicks ; Loads in the LeadMon nickname
+ call GetPartyMonName ; Sets name to be referenced by wNameBuffer in text\CeladonMart1F.asm
+ ld hl, DVCheckerOfferDVCheckText ; DV Checker mentions your LeadMon by name in his text
+ call PrintText
+ call YesNoChoice ; YES/NO menu pops up
+ ld a, [wCurrentMenuItem]
+ and a
+ jp nz, .RefusedDVCheck ; If player chooses 'NO', conversation ends
+ ld hl, DVCheckerVeryWellText ; If player chooses 'YES', begin process of deriving DVs
+ call PrintText
+.FindAttackDVParity ; begins lengthy process of getting HP DV
+ ld a, [$D186] ; loads address for Attack and Defense DVs
+ swap a ; moves Attack DV to low nybble
+ and %00000001 ; Attack DV LSB isolated: 'a' is now either 1 (odd) or 0 (even)
+ cp $1 ; is Attack DV ('a') odd or even?
+ jr nc, .AddAttackDVBonusToHPDV ; if 'a' is odd, add points to HP DV
+ jr .FindDefenseDVParity ; if 'a' is even, add nothing and skip to Defense DV evaluation
+.AddAttackDVBonusToHPDV
+ ld a, 0 ; set 'a' to 0 (starting point for tallying the HP DV)
+ ld b, 8 ; set 'b' to 8
+ add b ; 'a' is now 8 after adding 'a' and 'b'
+.FindDefenseDVParity
+ ld c, a ; store value of 'a' in 'c', which will hold our running tally for HP DV
+ ld a, [$D186] ; loads address for Attack and Defense DVs (no swap, already low nybble)
+ and %00000001 ; Defense DV LSB isolated: 'a' is now either 1 (odd) or 0 (even)
+ cp $1 ; is Defense DV ('a') odd or even?
+ jr nc, .AddDefenseDVBonusToHPDV ; if 'a' is odd, add points to HP DV
+ jr .FindSpeedDVParity ; if 'a' is even, add nothing and skip to Speed DV evaluation
+.AddDefenseDVBonusToHPDV
+ ld a, c ; return value in 'c' (our running tally) to 'a'
+ ld b, 4 ; 'b' is now 4
+ add b ; 'a' is now increased by 4
+ ld c, a ; store updated HP DV value back in 'c' for safe keeping
+.FindSpeedDVParity
+ ld a, [$D187] ; loads address for Speed and Special DVs
+ swap a ; moves Speed DV to low nybble
+ and %00000001 ; Speed DV LSB isolated: 'a' is now either 1 (odd) or 0 (even)
+ cp $1 ; is Speed DV ('a') odd or even?
+ jr nc, .AddSpeedDVBonusToHPDV ; if 'a' is odd, add points to HP DV
+ jr .FindSpecialDVParity ; if 'a' is even, add nothing and skip to Special DV evaluation
+.AddSpeedDVBonusToHPDV
+ ld a, c ; return value in 'c' (our running tally) to 'a'
+ ld b, 2 ; 'b' is now 2
+ add b ; 'a' is now increased by 2
+ ld c, a ; store updated HP DV value back in 'c' for safe keeping
+.FindSpecialDVParity
+ ld a, [$D187] ; loads address for Speed and Special DVs (no swap, already low nybble)
+ and %00000001 ; Special DV LSB isolated: 'a' is now either 1 (odd) or 0 (even)
+ cp $1 ; is Special DV ('a') odd or even?
+ jr nc, .AddSpecialDVBonusToHPDV ; if 'a' is odd, add points to HP DV
+ jr .FindHPDV ; if 'a' is even, add nothing and skip to finding final value of HP DV
+.AddSpecialDVBonusToHPDV
+ ld a, c ; return value in 'c' (our running tally) to 'a'
+ ld b, 1 ; 'b' is now 1
+ add b ; 'a' is now increased by 1
+ ld c, a ; store updated HP DV value back in 'c' for safe keeping
+.FindHPDV
+ ld a, c ; at last, move final HP DV value from 'c' to 'a'
+ ld [wBuffer], a ; loads HP DV into wBuffer to be used in text\CeladonMart1F.asm
+ ld hl, DVCheckerHPDVText ; FINALLY, the HP DV is ready to be read to the player
+ call PrintText
+.FindAttackDV ; thankfully, the other DVs are MUCH more straightforward...
+ ld a, [$D186] ; loads address for Attack and Defense DVs
+ and $F0 ; sets last 4 bits to zero, 'a' now contains [Attack DV bits][0000]
+ swap a ; swaps high and low nybbles, 'a' now contains [0000][Attack DV bits]
+ ld [wBuffer], a ; loads Attack DV into wBuffer
+ ld hl, DVCheckerAttackDVText ; Attack DV is now read to the player
+ call PrintText
+.FindDefenseDV
+ ld a, [$D186] ; loads address for Attack and Defense DVs
+ and $0F ; sets first 4 bits to zero, 'a' now contains [0000][Defense DV bits]
+ ld [wBuffer], a ; loads Defense DV into wBuffer
+ ld hl, DVCheckerDefenseDVText ; Defense DV is now read to the player
+ call PrintText
+.FindSpeedDV
+ ld a, [$D187] ; loads address for Speed and Special DVs
+ and $F0 ; sets last 4 bits to zero, 'a' now contains [Speed DV bits][0000]
+ swap a ; swaps high and low nybbles, 'a' now contains [0000][Speed DV bits]
+ ld [wBuffer], a ; loads Speed DV into wBuffer
+ ld hl, DVCheckerSpeedDVText ; Speed DV is now read to the player
+ call PrintText
+.FindSpecialDV
+ ld a, [$D187] ; loads address for Speed and Special DVs
+ and $0F ; sets first 4 bits to zero, 'a' now contains [0000][Special DV bits]
+ ld [wBuffer], a ; loads Special DV into wBuffer
+ ld hl, DVCheckerSpecialDVText ; Special DV is now read to the player
+ jr .DVCheckerDone ; script ends
+
+.RefusedDVCheck ; from here on we're just assigning the text for the
+ ld hl, DVCheckerSomeOtherTimeText ; DV Checker at every stage of your interaction
+ jr .DVCheckerDone
+
+.DVCheckerDone
+ call PrintText
+ jp TextScriptEnd
+
+DVCheckerOfferDVCheckText:
+ text_far _DVCheckerOfferDVCheckText
+ text_end
+
+DVCheckerSomeOtherTimeText:
+ text_far _DVCheckerSomeOtherTimeText
+ text_end
+
+DVCheckerVeryWellText:
+ text_far _DVCheckerVeryWellText
+ text_end
+
+DVCheckerHPDVText:
+ text_far _DVCheckerHPDVText
+ text_end
+
+DVCheckerAttackDVText:
+ text_far _DVCheckerAttackDVText
+ text_end
+
+DVCheckerDefenseDVText:
+ text_far _DVCheckerDefenseDVText
+ text_end
+
+DVCheckerSpeedDVText:
+ text_far _DVCheckerSpeedDVText
+ text_end
+
+DVCheckerSpecialDVText:
+ text_far _DVCheckerSpecialDVText
+ text_end
+
CeladonMart1FDirectorySignText:
text_far _CeladonMart1FDirectorySignText
text_end
...
Hopefully the annotations made that a little easier to understand. A couple of points to clarify, for those who need it:
and %00000001sets the first seven bits to 0, but it does NOT automatically set the last bit (the LSB) to 1; rather, the '1' at the end of the instruction simply indicates that the LSB be left alone.- Leading zeroes (such as the commented line
[0000][Attack DV bits]) have no effect on the value of a number. If a Pokemon's Attack DV is 1001 in binary (equal to 9 in decimal), then adding zeroes in front of it doesn't matter. In binary, '00001001' is still the same thing as '1001', just as '09' would be the same as '9' in decimal. This is why the game can read [0000][Attack DV bits] with no issue. The leading zeroes are simply ignored.
Now finally, the text file...
Create the DV Checker Text
Now open \text\CeladonMart1F.asm then Copy/Paste the following:
NOTE: As before with the scripts, place your object_event text before your bg_event text. I'm actually not sure if it's important for a text file, but better safe than sorry.
_CeladonMart1FReceptionistText::
text "Hello! Welcome to"
line "CELADON DEPT."
cont "STORE."
para "The board on the"
line "right describes"
cont "the store layout."
done
+_DVCheckerOfferDVCheckText::
+ text "Folks call me the"
+ line "DV CHECKER."
+
+ para "I can check the"
+ line "DVs of your lead"
+ cont "#MON."
+
+ para "Would you like me"
+ line "to evaluate your"
+ cont "@"
+ text_ram wNameBuffer
+ text "?"
+ done
+
+_DVCheckerSomeOtherTimeText::
+ text "Some other time,"
+ line "perhaps?"
+ done
+
+_DVCheckerVeryWellText::
+ text "Very well, then!"
+
+ para "Each stat gets"
+ line "scored 0 to 15."
+
+ para "Let's see..."
+
+ para "The scores for"
+ line "@"
+ text_ram wNameBuffer
+ text " are..."
+ prompt
+
+_DVCheckerHPDVText::
+ text "@"
+ text_decimal wBuffer, 1, 2 ; formatted as text_decimal [address to read], [# of bytes to read], [# of digits to dislpay]
+ text " HP..."
+ prompt
+
+_DVCheckerAttackDVText::
+ text "@"
+ text_decimal wBuffer, 1, 2
+ text " ATTACK..."
+ prompt
+
+_DVCheckerDefenseDVText::
+ text "@"
+ text_decimal wBuffer, 1, 2
+ text " DEFENSE..."
+ prompt
+
+_DVCheckerSpeedDVText::
+ text "@"
+ text_decimal wBuffer, 1, 2
+ text " SPEED..."
+ prompt
+_DVCheckerSpecialDVText::
+ text "@"
+ text_decimal wBuffer, 1, 2
+ text " SPECIAL..."
+
+ para "That's that! Come"
+ line "back any time!"
+ done
+
_CeladonMart1FDirectorySignText::
...
Now make your game, go talk to the DV Checker, and find out how bad your Pokemon stink!
Conclusion
Thanks for following along. If anyone wants to contribute the following, please do!
- How to give the player the option to select a Pokemon for evaluation, rather than just defaulting to the lead Pokemon
- Optimizing/condensing the code to find the HP DV. I just feel like there's a more efficient way...