Replace stat experience with EVs - pret/pokecrystal GitHub Wiki

Gen 3 replaced stat experience with EVs, which are different in a number of ways. We'll see those differences in this tutorial.

(EVs have an advantage outside of game mechanics: they take up fewer bytes. You'll end up with four unused bytes in the Pokémon data structure which can be used for all kinds of permanent data.)

Contents

  1. Replace stat experience with EVs in the Pokémon data structure
  2. Replace stat experience with EVs in base data
  3. Gain EVs from winning battles
  4. Calculate stats based on EVs
  5. Vitamins give EVs, not stat experience
  6. Replace Odd Egg and Battle Tower stat experience with EVs
  7. Replace MON_STAT_EXP with MON_EVS everywhere
  8. Replace some more labels
  9. Remove unused square root code
  10. Add Zinc to boost Special Defense EVs
  11. Limit total EVs to 510
  12. Replace stat experience with EVs in the Debug Room

1. Replace stat experience with EVs in the Pokémon data structure

Stat experience for each stat is a two-byte quantity from 0 to 65,535, with a single Special stat experience shared between Special Attack and Special Defense. EVs for each stat are one byte, from 0 to 255 (actually 252), with independent Special Attack and Special Defense quantities.

Edit macros/ram.asm:

 MACRO box_struct
 \1Species::        db
 \1Item::           db
 \1Moves::          ds NUM_MOVES
 \1ID::             dw
 \1Exp::            ds 3
-\1StatExp::
-\1HPExp::          dw
-\1AtkExp::         dw
-\1DefExp::         dw
-\1SpdExp::         dw
-\1SpcExp::         dw
+\1EVs::
+\1HPEV::           db
+\1AtkEV::          db
+\1DefEV::          db
+\1SpdEV::          db
+\1SpclAtkEV::      db
+\1SpclDefEV::      db
+\1Padding::        ds 4
 \1DVs::            dw
 \1PP::             ds NUM_MOVES
 \1Happiness::      db
 \1PokerusStatus::  db
 \1CaughtData::
 \1CaughtTime::
 \1CaughtLevel::    db
 \1CaughtGender::
 \1CaughtLocation:: db
 \1Level::          db
 \1End::
 ENDM

And edit constants/pokemon_data_constants.asm:

 ; party_struct members (see macros/ram.asm)
 rsreset
 DEF MON_SPECIES            rb
 DEF MON_ITEM               rb
 DEF MON_MOVES              rb NUM_MOVES
 DEF MON_ID                 rw
 DEF MON_EXP                rb 3
-DEF MON_STAT_EXP           rw NUM_EXP_STATS
-rsset MON_STAT_EXP
-DEF MON_HP_EXP             rw
-DEF MON_ATK_EXP            rw
-DEF MON_DEF_EXP            rw
-DEF MON_SPD_EXP            rw
-DEF MON_SPC_EXP            rw
+DEF MON_EVS                rb NUM_STATS
+rsset MON_EVS
+DEF MON_HP_EV              rb
+DEF MON_ATK_EV             rb
+DEF MON_DEF_EV             rb
+DEF MON_SPD_EV             rb
+DEF MON_SAT_EV             rb
+DEF MON_SDF_EV             rb
+                           rb_skip 4
 DEF MON_DVS                rw
 ...
 DEF PARTYMON_STRUCT_LENGTH EQU _RS

 DEF NICKNAMED_MON_STRUCT_LENGTH EQU PARTYMON_STRUCT_LENGTH + MON_NAME_LENGTH
 DEF REDMON_STRUCT_LENGTH EQU 44

 ...

+; significant EV values
+DEF MAX_EV EQU 252

By replacing the 10 stat experience bytes with 6 EV bytes, we've freed up 4 bytes in box_struct. That's valuable space, since it gets saved when Pokémon are deposited in the PC. Making use of it is beyond the scope of this tutorial, so we'll leave it as padding for now.

2. Replace stat experience with EVs in base data

When you knock out a Pokémon, the stat experience you gain is equal to its base stats. That doesn't work for EVs; each species has its own set of EV yields, with a gain of 0 to 3 for each stat. That means we can store each stat's gain in two bits, so six stats will fit in two bytes. Conveniently, there are two unused bytes in base data that we can replace.

Edit ram/wram.asm:

 ; corresponds to the data/pokemon/base_stats/*.asm contents
 wCurBaseData::
 wBaseDexNo:: db
 wBaseStats::
 wBaseHP:: db
 wBaseAttack:: db
 wBaseDefense:: db
 wBaseSpeed:: db
 wBaseSpecialAttack:: db
 wBaseSpecialDefense:: db
+wBaseEVs::
+wBaseHPAtkDefSpdEVs:: db
+wBaseSpAtkSpDefEVs:: db
 wBaseType::
 wBaseType1:: db
 wBaseType2:: db
 wBaseCatchRate:: db
 wBaseExp:: db
 wBaseItems::
 wBaseItem1:: db
 wBaseItem2:: db
 wBaseGender:: db
-wBaseUnknown1:: db
 wBaseEggSteps:: db
-wBaseUnknown2:: db
 wBasePicSize:: db
 wBasePadding:: ds 4
 wBaseGrowthRate:: db
 wBaseEggGroups:: db
 wBaseTMHM:: flag_array NUM_TM_HM_TUTOR
 wCurBaseDataEnd::
 	assert wCurBaseDataEnd - wCurBaseData == BASE_DATA_SIZE

Edit constants/pokemon_data_constants.asm:

 ; base data struct members (see data/pokemon/base_stats/*.asm)
 rsreset
 DEF BASE_DEX_NO      rb
 DEF BASE_STATS       rb NUM_STATS
 rsset BASE_STATS
 DEF BASE_HP          rb
 DEF BASE_ATK         rb
 DEF BASE_DEF         rb
 DEF BASE_SPD         rb
 DEF BASE_SAT         rb
 DEF BASE_SDF         rb
+DEF BASE_EVS         rw
+rsset BASE_EVS
+DEF BASE_HP_ATK_DEF_SPD_EVS  rb
+DEF BASE_SAT_SDF_EVS         rb
 DEF BASE_TYPES       rw
 rsset BASE_TYPES
 DEF BASE_TYPE_1      rb
 DEF BASE_TYPE_2      rb
 DEF BASE_CATCH_RATE  rb
 DEF BASE_EXP         rb
 DEF BASE_ITEMS       rw
 rsset BASE_ITEMS
 DEF BASE_ITEM_1      rb
 DEF BASE_ITEM_2      rb
 DEF BASE_GENDER      rb
-                 rb_skip
 DEF BASE_EGG_STEPS   rb
-                 rb_skip
 DEF BASE_PIC_SIZE    rb
 DEF BASE_FRONTPIC    rw
 DEF BASE_BACKPIC     rw
 DEF BASE_GROWTH_RATE rb
 DEF BASE_EGG_GROUPS  rb
 DEF BASE_TMHM        rb (NUM_TM_HM_TUTOR + 7) / 8
 DEF BASE_DATA_SIZE EQU _RS

Edit data/pokemon/base_stats.asm:

+MACRO evs
+	db (\1 << 6) | (\2 << 4) | (\3 << 2) | \4
+	db (\5 << 6) | (\6 << 4)
+ENDM

 ; used in data/pokemon/base_stats/*.asm
 MACRO tmhm
 ...

Finally, edit all 251 data/pokemon/base_stats/*.asm files. With each one, delete the unknown 1 and unknown 2 bytes and add evs after base stats. For example, here's data/pokemon/base_stats/chikorita.asm:

 	db CHIKORITA ; 152

 	db  45,  49,  65,  45,  49,  65
+	evs  0,   0,   0,   0,   0,   1
 	;   hp  atk  def  spd  sat  sdf

 	db GRASS, GRASS ; type
 	db 45 ; catch rate
 	db 64 ; base exp
 	db NO_ITEM, NO_ITEM ; items
 	db GENDER_F12_5 ; gender ratio
-	db 100 ; unknown 1
 	db 20 ; step cycles to hatch
-	db 5 ; unknown 2
 	INCBIN "gfx/pokemon/chikorita/front.dimensions"
 	dw NULL, NULL ; unused (beta front/back pics)
 	db GROWTH_MEDIUM_SLOW ; growth rate
 	dn EGG_MONSTER, EGG_PLANT ; egg groups

 	; tm/hm learnset
 	...

You can do this automatically with a Python script. Save this as base-evs.py in the same directory as main.asm:

import glob

filenames = glob.glob('data/pokemon/base_stats/*.asm')

for filename in filenames:

	print('Update', filename)

	with open(filename, 'r', encoding='utf8') as file:
		lines = file.readlines()

	with open(filename, 'w', encoding='utf8') as file:
		for line in lines:
			if line in ['\tdb 100 ; unknown 1\n', '\tdb 5 ; unknown 2\n']:
				continue
			if line == '\t;   hp  atk  def  spd  sat  sdf\n':
				file.write('\tevs  0,   0,   0,   0,   0,   0\n')
			file.write(line)

Then run python3 base-evs.py, just like running make. It should output:

$ python3 base-evs.py
Update data/pokemon/base_stats/abra.asm
...
Update data/pokemon/base_stats/zubat.asm

(If it gives an error "python3: command not found", you need to install Python 3. It's available as the python3 package in Cygwin.)

That will format all the base data files correctly, but with zero EV yields. To get the proper EV yields, you can use a Python script made by Nayru62.

Otherwise, you'll have to fill in the correct values yourself.

3. Gain EVs from winning battles

Edit engine/battle/core.asm:

 GiveExperiencePoints:
	...

-; give stat exp
-	ld hl, MON_STAT_EXP + 1
-	add hl, bc
-	ld d, h
-	ld e, l
-	ld hl, wEnemyMonBaseStats - 1
-	push bc
-	ld c, NUM_EXP_STATS
-.stat_exp_loop
-	inc hl
-	ld a, [de]
-	add [hl]
-	ld [de], a
-	jr nc, .no_carry_stat_exp
-	dec de
-	ld a, [de]
-	inc a
-	jr z, .stat_exp_maxed_out
-	ld [de], a
-	inc de
-
-.no_carry_stat_exp
-	push hl
-	push bc
-	ld a, MON_POKERUS
-	call GetPartyParamLocation
-	ld a, [hl]
-	and a
-	pop bc
-	pop hl
-	jr z, .stat_exp_awarded
-	ld a, [de]
-	add [hl]
-	ld [de], a
-	jr nc, .stat_exp_awarded
-	dec de
-	ld a, [de]
-	inc a
-	jr z, .stat_exp_maxed_out
-	ld [de], a
-	inc de
-	jr .stat_exp_awarded
-
-.stat_exp_maxed_out
-	ld a, $ff
-	ld [de], a
-	inc de
-	ld [de], a
-
-.stat_exp_awarded
-	inc de
-	inc de
-	dec c
-	jr nz, .stat_exp_loop
+; Give EVs
+; e = 0 for no Pokérus, 1 for Pokérus
+	ld e, 0
+	ld hl, MON_POKERUS
+	add hl, bc
+	ld a, [hl]
+	and a
+	jr z, .no_pokerus
+	inc e
+.no_pokerus
+	ld hl, MON_EVS
+	add hl, bc
+	push bc
+	ld a, [wEnemyMonSpecies]
+	ld [wCurSpecies], a
+	call GetBaseData
+; EV yield format: %hhaaddss %ttff0000
+; h = hp, a = atk, d = def, s = spd
+; t = sat, f = sdf, 0 = unused bits
+	ld a, [wBaseHPAtkDefSpdEVs]
+	ld b, a
+	ld c, NUM_STATS ; six EVs
+.ev_loop
+	rlc b
+	rlc b
+	ld a, b
+	and %11
+	bit 0, e
+	jr z, .no_pokerus_boost
+	add a
+.no_pokerus_boost
+	add [hl]
+	jr c, .ev_overflow
+	cp MAX_EV + 1
+	jr c, .got_ev
+.ev_overflow
+	ld a, MAX_EV
+.got_ev
+	ld [hli], a
+	dec c
+	jr z, .evs_done
+; Use the second byte for Sp.Atk and Sp.Def
+	ld a, c
+	cp 2 ; two stats left, Sp.Atk and Sp.Def
+	jr nz, .ev_loop
+	ld a, [wBaseSpAtkSpDefEVs]
+	ld b, a
+	jr .ev_loop
+.evs_done
	...

 .EvenlyDivideExpAmongParticipants:
	...
	ld [wTempByteValue], a
-	ld hl, wEnemyMonBaseStats
-	ld c, wEnemyMonEnd - wEnemyMonBaseStats
-.base_stat_division_loop
+	ld hl, wEnemyMonBaseExp
	xor a
	ldh [hDividend + 0], a
	ld a, [hl]
	ldh [hDividend + 1], a
	ld a, [wTempByteValue]
	ldh [hDivisor], a
	ld b, 2
	call Divide
	ldh a, [hQuotient + 3]
-	ld [hli], a
-	dec c
-	jr nz, .base_stat_division_loop
+	ld [hl], a
	ret

Now instead of gaining the enemy's base stats toward your stat experience, you'll gain their base EV yields toward your EV totals. Having Pokérus will double EV gain.

Also, since we're not using stat experience, we no longer need to divide the enemy's base stats among the battle participants.

4. Calculate stats based on EVs

Edit engine/pokemon/move_mon.asm:

 CalcMonStats:
 ; Calculates all 6 Stats of a mon
-; b: Take into account stat EXP if TRUE
+; b: Take into account EVs if TRUE
 ; 'c' counts from 1-6 and points with 'wBaseStats' to the base value
-; hl is the path to the Stat EXP
+; hl is the path to the EVs
 ; de points to where the final stats will be saved

 	ld c, STAT_HP - 1 ; first stat
 .loop
 	inc c
 	call CalcMonStatC
 	ldh a, [hMultiplicand + 1]
 	ld [de], a
 	inc de
 	ldh a, [hMultiplicand + 2]
 	ld [de], a
 	inc de
 	ld a, c
 	cp STAT_SDEF ; last stat
 	jr nz, .loop
 	ret

 CalcMonStatC:
 ; 'c' is 1-6 and points to the BaseStat
 ; 1: HP
 ; 2: Attack
 ; 3: Defense
 ; 4: Speed
 ; 5: SpAtk
 ; 6: SpDef
 	push hl
 	push de
 	push bc
 	ld a, b
 	ld d, a
 	push hl
 	ld hl, wBaseStats
 	dec hl ; has to be decreased, because 'c' begins with 1
 	ld b, 0
 	add hl, bc
 	ld a, [hl]
 	ld e, a
 	pop hl
 	push hl
- 	ld a, c
- 	cp STAT_SDEF ; last stat
- 	jr nz, .not_spdef
- 	dec hl
- 	dec hl
-
- .not_spdef
- 	sla c
 	ld a, d
 	and a
 	jr z, .no_stat_exp
 	add hl, bc
-	push de
-	ld a, [hld]
-	ld e, a
-	ld d, [hl]
-	farcall GetSquareRoot
-	pop de
+	ld a, [hl]
+	ld b, a

 .no_stat_exp
-	srl c
 	pop hl
 	push bc
-	ld bc, MON_DVS - MON_HP_EXP + 1
+	ld bc, MON_DVS - MON_HP_EV + 1
 	add hl, bc
 	pop bc
 	...

The CalcMonStatC implements these formulas for stat values:

  • HP = (((base + IV) × 2 + √exp / 4) × level) / 100 + level + 10
  • stat = (((base + IV) × 2 + √exp / 4) × level) / 100 + 5

In those formulas, division rounds down and square root rounds up (for example, √12 = 3.4641… rounds to 4). Order of operations is standard PEMDAS.

Anyway, we've just replaced √exp in those formulas with simply EV.

This change has consequences for progressing through the game. Square roots are nonlinear, so early gains to stat experience were contributing relatively larger boosts to stats. But EVs are linear, so gaining 4 EVs will be just as beneficial no matter how many you already had.

For example, 50 EVs are equivalent to 50² = 2,500 stat exp, and 100 EVs are equivalent to 100² = 10,000 stat exp. But getting from 50 EVs to 100 takes the same effort as from 0 to 50, whereas getting from 2,500 to 10,000 stat exp means gaining another 7,500 stat exp: three times as much effort as the first 2,500.

Eventually this won't matter, since the maximum 252 EVs or 65,535 stat exp both result in the same stats (252 / 4 = √65,535 / 4 = 63). But you may notice your Pokémon stats growing more slowly at first, and more quickly later on than you're used to.

5. Vitamins give EVs, not stat experience

Edit engine/items/item_effects.asm:

 VitaminEffect:
 	ld b, PARTYMENUACTION_HEALING_ITEM
 	call UseItem_SelectMon

 	jp c, RareCandy_StatBooster_ExitMenu

 	call RareCandy_StatBooster_GetParameters

-	call GetStatExpRelativePointer
+	call GetEVRelativePointer

-	ld a, MON_STAT_EXP
+	ld a, MON_EVS
 	call GetPartyParamLocation

 	add hl, bc
 	ld a, [hl]
 	cp 100
 	jr nc, NoEffectMessage

 	add 10
 	ld [hl], a
 	call UpdateStatsAfterItem

-	call GetStatExpRelativePointer
+	call GetEVRelativePointer

 	ld hl, StatStrings
 	add hl, bc
+	add hl, bc
 	ld a, [hli]
 	ld h, [hl]
 	ld l, a
 	ld de, wStringBuffer2
 	ld bc, ITEM_NAME_LENGTH
 	call CopyBytes

 	...

 StatStrings:
 	dw .health
 	dw .attack
 	dw .defense
 	dw .speed
-	dw .special
+	dw .sp_atk

 .health  db "HEALTH@"
 .attack  db "ATTACK@"
 .defense db "DEFENSE@"
 .speed   db "SPEED@"
-.special db "SPECIAL@"
+.sp_atk  db "SPCL.ATK@"

-GetStatExpRelativePointer:
+GetEVRelativePointer:
 	ld a, [wCurItem]
-	ld hl, StatExpItemPointerOffsets
+	ld hl, EVItemPointerOffsets
 	...

-StatExpItemPointerOffsets:
-	db HP_UP,    MON_HP_EXP - MON_STAT_EXP
-	db PROTEIN, MON_ATK_EXP - MON_STAT_EXP
-	db IRON,    MON_DEF_EXP - MON_STAT_EXP
-	db CARBOS,  MON_SPD_EXP - MON_STAT_EXP
-	db CALCIUM, MON_SPC_EXP - MON_STAT_EXP
+EVItemPointerOffsets:
+	db HP_UP,    MON_HP_EV - MON_EVS
+	db PROTEIN, MON_ATK_EV - MON_EVS
+	db IRON,    MON_DEF_EV - MON_EVS
+	db CARBOS,  MON_SPD_EV - MON_EVS
+	db CALCIUM, MON_SAT_EV - MON_EVS

Vitamins used to give 2,560 stat experience, up to a maximum of 25,600. Now they give 10 EVs, up to a maximum of 100. Conveniently, the vitamin code already used the values 10 and 100, because those are the high bytes of 2,560 and 25,600.

Due to that convenience, this mostly involved changing label and constant names. The only real adjustment needed was the offset to StatStrings: stat experience and string pointers were both two-byte values, but now EVs are one byte, so we needed a second add hl, bc to get the stat string corresponding to an EV.

We also replaced "SPECIAL" with "SPCL.ATK" since Calcium only affects the Special Attack EV. The same should be done for the description of Calcium.

Edit data/items/descriptions.asm:

 CalciumDesc:
-	db   "Ups SPECIAL stats"
+	db   "Raises SPCL.ATK"
 	next "of one #MON.@"

6. Replace Odd Egg and Battle Tower stat experience with EVs

First, edit data/events/odd_eggs.asm. Make this same replacement 14 times, once for each hard-coded Odd Egg Pokémon structure:

-	; Stat exp
-	bigdw 0
-	bigdw 0
-	bigdw 0
-	bigdw 0
-	bigdw 0
+	db 0, 0, 0, 0, 0, 0 ; EVs
+	db 0, 0, 0, 0 ; padding

Next, edit data/battle_tower/parties.asm. This is trickier for two reasons. One, there are 210 Pokémon structures instead of 14. Two, they have nonzero stat experience, and their hard-coded stats need to match their new EV values. For example:

	db JOLTEON
	db MIRACLEBERRY
	db THUNDERBOLT, HYPER_BEAM, SHADOW_BALL, ROAR
	dw 0 ; OT ID
	dt 1000 ; Exp
-	; Stat exp
-	bigdw 50000
-	bigdw 40000
-	bigdw 40000
-	bigdw 35000
-	bigdw 40000
+	db 224, 200, 200, 188, 200, 200 ; EVs
+	db 0, 0, 0, 0 ; padding
	dn 13, 13, 11, 13 ; DVs
	db 15, 5, 15, 20 ; PP
	db 100 ; Happiness
	db 0, 0, 0 ; Pokerus, Caught data
	db 10 ; Level
	db 0, 0 ; Status
	bigdw 41 ; HP
	bigdw 41 ; Max HP
	bigdw 25 ; Atk
	bigdw 24 ; Def
	bigdw 37 ; Spd
	bigdw 34 ; SAtk
	bigdw 31 ; SDef
	db "SANDA-SU@@@"

Numerically speaking, you just have to take the square root of each stat experience value and round up to an integer EV; but you have to do this for 210 × 5 values, and insert padding bytes.

You can do this automatically with a Python script. Save this as bt-evs.py in the same directory as main.asm:

from math import sqrt, ceil

def derive_ev(stat_exp_line):
	stat_exp = int(stat_exp_line[len('\tbigdw '):])
	return str(int(ceil(sqrt(stat_exp))))

filename = 'data/battle_tower/parties.asm'

with open(filename, 'r', encoding='utf8') as file:
	lines = file.readlines()

with open(filename, 'w', encoding='utf8') as file:
	i = 0
	while i < len(lines):
		line = lines[i]

		if line != '\t; Stat exp\n':
			file.write(line)
			i += 1
			continue

		exp_lines = lines[i+1:i+6]
		evs = [derive_ev(exp_line) for exp_line in exp_lines]
		evs.append(evs[-1]) # Special -> Sp.Atk and Sp.Def
		file.write('\tdb {} ; EVs\n'.format(', '.join(evs)))
		file.write('\tdb 0, 0, 0, 0 ; padding\n')
		i += 6

print('Done!')

Then run python3 bt-evs.py. It should output:

$ python3 battle-tower-evs.py
Done!

7. Replace MON_STAT_EXP with MON_EVS everywhere

Replace every occurrence of MON_STAT_EXP with MON_EVS in these files:

Most of the MON_STAT_EXP occurrences are part of an argument passed to CalcMonStats.

8. Replace some more labels

Edit engine/events/daycare.asm:

 DayCare_InitBreeding:
 	...
 	xor a
-	ld b, wEggMonDVs - wEggMonStatExp
-	ld hl, wEggMonStatExp
+	ld b, wEggMonDVs - wEggMonEVs
+	ld hl, wEggMonEVs
 .loop2
 	ld [hli], a
 	dec b
 	jr nz, .loop2

We're technically done now; EVs will work behind the scenes just like stat experience did. But there's room for more improvement.

9. Remove unused square root code

The only place GetSquareRoot was used was in CalcMonStatC. Without that, we can safely remove it.

Delete engine/math/get_square_root.asm.

Then edit main.asm:

-INCLUDE "engine/math/get_square_root.asm"

10. Add Zinc to boost Special Defense EVs

Now that Calcium only boosts Special Attack EVs, we need Zinc for Special Defense. Follow this tutorial to add a new item.

First, add the essential data. Replace ITEM_89 with ZINC; give it a name, description, and attributes (9800, HELD_NONE, 0, CANT_SELECT, ITEM, ITEMMENU_PARTY, ITEMMENU_NOUSE); and give it the VitaminEffect. (ITEM_89 is not in TimeCapsule_CatchRateItems.)

Then edit engine/items/item_effects.asm again:

 StatStrings:
 	dw .health
 	dw .attack
 	dw .defense
 	dw .speed
 	dw .sp_atk
+	dw .sp_def

 .health  db "HEALTH@"
 .attack  db "ATTACK@"
 .defense db "DEFENSE@"
 .speed   db "SPEED@"
 .sp_atk  db "SPCL.ATK@"
+.sp_def  db "SPCL.DEF@"

 ...

 EVItemPointerOffsets:
 	db HP_UP,    MON_HP_EV - MON_EVS
 	db PROTEIN, MON_ATK_EV - MON_EVS
 	db IRON,    MON_DEF_EV - MON_EVS
 	db CARBOS,  MON_SPD_EV - MON_EVS
 	db CALCIUM, MON_SAT_EV - MON_EVS
+	db ZINC,    MON_SDF_EV - MON_EVS

That's all!

Screenshot

11. Limit total EVs to 510

At this point all stats use EVs instead of Stat Exp, but the total EV limit form Gen 3 onward hasn't been coded yet; in this section we're going to implement it. First, edit constants/pokemon_data_constants.asm again:

 ; significant EV values
 DEF MAX_EV EQU 252
+DEF MAX_TOTAL_EV EQU 510

Next edit GiveExperiencePoints in engine/battle/core.asm again:

	jr z, .no_pokerus_boost
	add a
 .no_pokerus_boost 
+; Make sure total EVs never surpass 510
+	push bc
+	push hl
+	ld d, a
+	ld a, c
+.find_correct_ev_address
+	; If address of first EV is changed, find the correct one.
+	cp NUM_STATS
+	jr z, .found_address
+	dec hl
+	inc a
+	jr .find_correct_ev_address
+.found_address
+	ld e, NUM_STATS
+	ld bc, 0
+.count_evs
+	ld a, [hli]
+	add c
+	ld c, a
+	jr nc, .cont
+	inc b
+.cont
+	dec e
+	jr nz, .count_evs
+	ld a, d
+	add c
+	ld c, a
+	adc b
+	sub c
+	ld b, a
+	ld e, d
+.decrease_evs_gained
+	call IsEvsGreaterThan510
+	jr nc, .check_ev_overflow
+	dec e
+	dec bc
+	jr .decrease_evs_gained
+.check_ev_overflow
+	pop hl 
+	pop bc 
+	ld a, e
	add [hl]
	jr c, .ev_overflow
	cp MAX_EV + 1

 ...

	ldh a, [hQuotient + 3]
	ld [hl], a
	ret

+IsEvsGreaterThan510:
+; Total EVs in bc. Set Carry flag if bc > 510.
+       ld a, HIGH(MAX_TOTAL_EV)
+	cp b
+	ret nz
+	ld a, LOW(MAX_TOTAL_EV)
+	cp c
+	ret

What this does is first count your Pokémon's total EVs, then add the EVs you would normally gain. If the total EVs after the battle is greater than 510, decrease the EVs you gained until the total EVs after the battle is 510.

But this change doesn't affect vitamins, thus, we need to do another edit.

Edit VitaminEffect in engine/items/item_effects.asm again:

 VitaminEffect:
	...
	ld a, MON_EVS
	call GetPartyParamLocation

+	ld d, 10
+	push bc
+	push hl
+	ld e, NUM_STATS
+	ld bc, 0
+.count_evs
+	ld a, [hli]
+	add c
+	ld c, a
+	jr nc, .cont
+	inc b
+.cont
+	dec e
+	jr nz, .count_evs
+	ld a, d
+	add c
+	ld c, a
+	adc b
+	sub c 
+	ld b, a
+	ld e, d
+.decrease_evs_gained
+	farcall IsEvsGreaterThan510
+	jr nc, .check_ev_overflow
+	dec e
+	dec bc
+	jr .decrease_evs_gained
+.check_ev_overflow
+	pop hl 
+	pop bc 

+	ld a, e
+	and a
+	jr z, NoEffectMessage

	add hl, bc
	ld a, [hl]
	cp 100
	jr nc, NoEffectMessage

-	add 10
+	add e
	ld [hl], a
	call UpdateStatsAfterItem
	...

This way, not only the limit of 510 EVs is implemented, but it will also display a message if the total EVs has reached the max limit.

12. Replace stat experience with EVs in the Debug Room

This is only relevant if you're building a debug ROM. If you're not, you can skip this step.

The "POKéMON GET!" option in the Debug Room creates a Pokémon by manually editing each field of its party_struct. We need to change the stat experience fields to EVs, otherwise the debug ROM can't be assembled.

Edit engine/debug/debug_room.asm:

 DebugRoomMenu_PokemonGet_Page2Values:
-	db 8
-	paged_value wDebugRoomMonHPExp+0,       $00, $ff,         $00,            DebugRoom_BoxStructStrings.HPExp0,    NULL,                       FALSE
-	paged_value wDebugRoomMonHPExp+1,       $00, $ff,         $00,            DebugRoom_BoxStructStrings.HPExp1,    NULL,                       FALSE
-	paged_value wDebugRoomMonAtkExp+0,      $00, $ff,         $00,            DebugRoom_BoxStructStrings.AttkExp0,  NULL,                       FALSE
-	paged_value wDebugRoomMonAtkExp+1,      $00, $ff,         $00,            DebugRoom_BoxStructStrings.AttkExp1,  NULL,                       FALSE
-	paged_value wDebugRoomMonDefExp+0,      $00, $ff,         $00,            DebugRoom_BoxStructStrings.DfnsExp0,  NULL,                       FALSE
-	paged_value wDebugRoomMonDefExp+1,      $00, $ff,         $00,            DebugRoom_BoxStructStrings.DfnsExp1,  NULL,                       FALSE
-	paged_value wDebugRoomMonSpdExp+0,      $00, $ff,         $00,            DebugRoom_BoxStructStrings.SpeedExp0, NULL,                       FALSE
-	paged_value wDebugRoomMonSpdExp+1,      $00, $ff,         $00,            DebugRoom_BoxStructStrings.SpeedExp1, NULL,                       FALSE
+	db 6
+	paged_value wDebugRoomMonHPEV,          $00, $ff,         $00,            DebugRoom_BoxStructStrings.HPEV,      NULL,                       FALSE
+	paged_value wDebugRoomMonAtkEV,         $00, $ff,         $00,            DebugRoom_BoxStructStrings.AttackEV,  NULL,                       FALSE
+	paged_value wDebugRoomMonDefEV,         $00, $ff,         $00,            DebugRoom_BoxStructStrings.DefenseEV, NULL,                       FALSE
+	paged_value wDebugRoomMonSpdEV,         $00, $ff,         $00,            DebugRoom_BoxStructStrings.SpeedEV,   NULL,                       FALSE
+	paged_value wDebugRoomMonSpclAtkEV,     $00, $ff,         $00,            DebugRoom_BoxStructStrings.SpclAtkEV, NULL,                       FALSE
+	paged_value wDebugRoomMonSpclDefEV,     $00, $ff,         $00,            DebugRoom_BoxStructStrings.SpclDefEV, NULL,                       FALSE
 

 DebugRoomMenu_PokemonGet_Page3Values:
-	db 8
-	paged_value wDebugRoomMonSpcExp+0,      $00, $ff,         $00,            DebugRoom_BoxStructStrings.SpclExp0,  NULL,                       FALSE
-	paged_value wDebugRoomMonSpcExp+1,      $00, $ff,         $00,            DebugRoom_BoxStructStrings.SpclExp1,  NULL,                       FALSE
+	db 6
 	paged_value wDebugRoomMonDVs+0,         $00, $ff,         $00,            DebugRoom_BoxStructStrings.PowerRnd0, NULL,                       TRUE
 	paged_value wDebugRoomMonDVs+1,         $00, $ff,         $00,            DebugRoom_BoxStructStrings.PowerRnd1, NULL,                       TRUE
 	paged_value wDebugRoomMonPP+0,          $00, $ff,         $00,            DebugRoom_BoxStructStrings.PP1,       NULL,                       FALSE
 	paged_value wDebugRoomMonPP+1,          $00, $ff,         $00,            DebugRoom_BoxStructStrings.PP2,       NULL,                       FALSE
 	paged_value wDebugRoomMonPP+2,          $00, $ff,         $00,            DebugRoom_BoxStructStrings.PP3,       NULL,                       FALSE
 	paged_value wDebugRoomMonPP+3,          $00, $ff,         $00,            DebugRoom_BoxStructStrings.PP4,       NULL,                       FALSE
 DebugRoom_BoxStructStrings:
 ...
-.HPExp0:    db "HP EXP[0]@"
-.HPExp1:    db "HP EXP[1]@"
-.AttkExp0:  db "ATTK EXP[0]@"
-.AttkExp1:  db "ATTK EXP[1]@"
-.DfnsExp0:  db "DFNS EXP[0]@"
-.DfnsExp1:  db "DFNS EXP[1]@"
-.SpeedExp0: db "SPEED EXP[0]@"
-.SpeedExp1: db "SPEED EXP[1]@"
-.SpclExp0:  db "SPCL EXP[0]@"
-.SpclExp1:  db "SPCL EXP[1]@"
+.HPEV:      db "HP EV@"
+.AttackEV:  db "ATTACK EV@"
+.DefenseEV: db "DEFENSE EV@"
+.SpeedEV:   db "SPEED EV@"
+.SpclAtkEV: db "SPCL ATK EV@"
+.SpclDefEV: db "SPCL DEF EV@"
 ...

TODO: add Macho Brace.