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](#1-replace-stat-experience-with-evs-in-the-pokémon-data-structure) 2. [Replace stat experience with EVs in base data](#2-replace-stat-experience-with-evs-in-base-data) 3. [Gain EVs from winning battles](#3-gain-evs-from-winning-battles) 4. [Calculate stats based on EVs](#4-calculate-stats-based-on-evs) 5. [Vitamins give EVs, not stat experience](#5-vitamins-give-evs-not-stat-experience) 6. [Replace Odd Egg and Battle Tower stat experience with EVs](#6-replace-odd-egg-and-battle-tower-stat-experience-with-evs) 7. [Replace `MON_STAT_EXP` with `MON_EVS` everywhere](#7-replace-mon_stat_exp-with-mon_evs-everywhere) 8. [Replace some more labels](#8-replace-some-more-labels) 9. [Remove unused square root code](#9-remove-unused-square-root-code) 10. [Add Zinc to boost Special Defense EVs](#10-add-zinc-to-boost-special-defense-evs) ## 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/wram.asm](../blob/master/macros/wram.asm): ```diff box_struct: MACRO \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](../blob/master/constants/pokemon_data_constants.asm): ```diff ; party_struct members (see macros/wram.asm) MON_SPECIES EQUS "(wPartyMon1Species - wPartyMon1)" MON_ITEM EQUS "(wPartyMon1Item - wPartyMon1)" MON_MOVES EQUS "(wPartyMon1Moves - wPartyMon1)" MON_ID EQUS "(wPartyMon1ID - wPartyMon1)" MON_EXP EQUS "(wPartyMon1Exp - wPartyMon1)" -MON_STAT_EXP EQUS "(wPartyMon1StatExp - wPartyMon1)" -MON_HP_EXP EQUS "(wPartyMon1HPExp - wPartyMon1)" -MON_ATK_EXP EQUS "(wPartyMon1AtkExp - wPartyMon1)" -MON_DEF_EXP EQUS "(wPartyMon1DefExp - wPartyMon1)" -MON_SPD_EXP EQUS "(wPartyMon1SpdExp - wPartyMon1)" -MON_SPC_EXP EQUS "(wPartyMon1SpcExp - wPartyMon1)" +MON_EVS EQUS "(wPartyMon1EVs - wPartyMon1)" +MON_HP_EV EQUS "(wPartyMon1HPEV - wPartyMon1)" +MON_ATK_EV EQUS "(wPartyMon1AtkEV - wPartyMon1)" +MON_DEF_EV EQUS "(wPartyMon1DefEV - wPartyMon1)" +MON_SPD_EV EQUS "(wPartyMon1SpdEV - wPartyMon1)" +MON_SAT_EV EQUS "(wPartyMon1SpclAtkEV - wPartyMon1)" +MON_SDF_EV EQUS "(wPartyMon1SpclDefEV - wPartyMon1)" +MON_PADDING EQUS "(wPartyMon1Padding - wPartyMon1)" MON_DVS EQUS "(wPartyMon1DVs - wPartyMon1)" ... BOXMON_STRUCT_LENGTH EQUS "(wPartyMon1End - wPartyMon1)" PARTYMON_STRUCT_LENGTH EQUS "(wPartyMon1StatsEnd - wPartyMon1)" REDMON_STRUCT_LENGTH EQU 44 ... +; significant EV values +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 [wram.asm](../blob/master/wram.asm): ```diff ; corresponds to the data/pokemon/base_stats/*.asm contents wCurBaseData:: ; d236 wBaseDexNo:: db ; d236 wBaseStats:: ; d237 wBaseHP:: db ; d237 wBaseAttack:: db ; d238 wBaseDefense:: db ; d239 wBaseSpeed:: db ; d23a wBaseSpecialAttack:: db ; d23b wBaseSpecialDefense:: db ; d23c +wBaseEVs:: +wBaseHPAtkDefSpdEVs:: db +wBaseSpAtkSpDefEVs:: db wBaseType:: ; d23d wBaseType1:: db ; d23d wBaseType2:: db ; d23e wBaseCatchRate:: db ; d23f wBaseExp:: db ; d240 wBaseItems:: ; d241 wBaseItem1:: db ; d241 wBaseItem2:: db ; d242 wBaseGender:: db ; d243 -wBaseUnknown1:: db ; d244 wBaseEggSteps:: db ; d245 -wBaseUnknown2:: db ; d246 wBasePicSize:: db ; d247 wBasePadding:: ds 4 ; d248 wBaseGrowthRate:: db ; d24c wBaseEggGroups:: db ; d24d wBaseTMHM:: flag_array NUM_TM_HM_TUTOR ; d24e wCurBaseDataEnd:: ``` Edit [data/pokemon/base_stats.asm](../blob/master/data/pokemon/base_stats.asm): ```diff +evs: MACRO + db (\1 << 6) | (\2 << 4) | (\3 << 2) | \4 + db (\5 << 6) | (\6 << 4) +ENDM tmhm: MACRO ... ``` Finally, edit all 251 [data/pokemon/base_stats/\*.asm](../tree/master/data/pokemon/base_stats/) 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](../blob/master/data/pokemon/base_stats/chikorita.asm): ```diff 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" db 0, 0, 0, 0 ; padding 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: ```python 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. You'll have to fill in the correct values yourself. ## 3. Gain EVs from winning battles Edit [engine/battle/core.asm](../blob/master/engine/battle/core.asm): ```diff 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 -.loop1 - inc hl - ld a, [de] - add [hl] - ld [de], a - jr nc, .okay1 - dec de - ld a, [de] - inc a - jr z, .next - ld [de], a - inc de - -.okay1 - push hl - push bc - ld a, MON_PKRUS - call GetPartyParamLocation - ld a, [hl] - and a - pop bc - pop hl - jr z, .skip - ld a, [de] - add [hl] - ld [de], a - jr nc, .skip - dec de - ld a, [de] - inc a - jr z, .next - ld [de], a - inc de - jr .skip - -.next - ld a, $ff - ld [de], a - inc de - ld [de], a - -.skip - inc de - inc de - dec c - jr nz, .loop1 +; Give EVs +; e = 0 for no Pokerus, 1 for Pokerus + ld e, 0 + ld hl, MON_PKRUS + 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, 6 ; 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 ``` 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. ## 4. Calculate stats based on EVs Edit [engine/pokemon/move_mon.asm](../blob/master/engine/pokemon/move_mon.asm): ```diff 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](https://en.wikipedia.org/wiki/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](../blob/master/engine/items/item_effects.asm): ```diff 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, Table_eeeb ... Table_eeeb: - 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 + 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](../blob/master/data/items/descriptions.asm): ```diff 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](../blob/master/data/events/odd_eggs.asm). Make this same replacement 14 times, once for each hard-coded Odd Egg Pokémon structure: ```diff - ; 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](../blob/master/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: ```diff 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: ```python 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: - [engine/battle/core.asm](../blob/master/engine/battle/core.asm) again (two, in `LoadEnemyMon` and `GiveExperiencePoints`) - [engine/pokemon/move_mon.asm](../blob/master/engine/pokemon/move_mon.asm) again (five; three in `GeneratePartyMonStats`, one in `SendGetMonIntoFromBox`, one in `ComputeNPCTrademonStats` - [engine/items/item_effects.asm](../blob/master/engine/items/item_effects.asm) again (one, in `UpdateStatsAfterItem`) - [engine/events/battle_tower/battle_tower.asm](../blob/master/engine/events/battle_tower/battle_tower.asm) (one, in `ValidateBTParty`) - [engine/link/link.asm](../blob/master/engine/link/link.asm) (three; one in `Link_PrepPartyData_Gen1`, two in `Function2868a`) - [engine/pokemon/breeding.asm](../blob/master/engine/pokemon/breeding.asm) (one, in `HatchEggs`) - [engine/pokemon/correct_party_errors.asm](../blob/master/engine/pokemon/correct_party_errors.asm) (one, in `Unreferenced_CorrectPartyErrors`) - [engine/pokemon/tempmon.asm](../blob/master/engine/pokemon/tempmon.asm) (one, in `_TempMonStatsCalculation`) - [mobile/mobile_46.asm](../blob/master/mobile/mobile_46.asm) (two; one in `Function11b483`, one in `Function11b6b4`) 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](../blob/master/engine/events/daycare.asm): ```diff 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](../blob/master/engine/math/get_square_root.asm). Then edit [main.asm](../blob/master/main.asm): ```diff -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](Add-a-new-item) 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](../blob/master/engine/items/item_effects.asm) again: ```diff 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@" ... Table_eeeb: 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](screenshots/zinc.png) TODO: limit total EVs to 510. TODO: add Macho Brace.