summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRangi <remy.oukaour+rangi42@gmail.com>2018-07-24 20:40:32 -0400
committerRangi <remy.oukaour+rangi42@gmail.com>2018-07-24 20:40:32 -0400
commit2d206be3d85de87eeb856397067ef14ba9bb780c (patch)
tree503031a7bd7f2737f4eab7fa60ad061e51025a87
parentc7f08d9f6e75a0f57c28eff6ce93f7202bc40f52 (diff)
Replace stat experience with EVs
-rw-r--r--Replace-stat-experience-with-EVs.md632
-rw-r--r--Tutorials.md2
-rw-r--r--screenshots/zinc.pngbin0 -> 1583 bytes
3 files changed, 633 insertions, 1 deletions
diff --git a/Replace-stat-experience-with-EVs.md b/Replace-stat-experience-with-EVs.md
new file mode 100644
index 0000000..c14e16c
--- /dev/null
+++ b/Replace-stat-experience-with-EVs.md
@@ -0,0 +1,632 @@
+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
+ ; hp atk def spd sat sdf
+
++ 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
+ ...
+```
+
+## 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
+ ld a, [hMultiplicand + 1]
+ ld [de], a
+ inc de
+ ld 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 has consequences for progressing through the game.
+
+Square roots are nonlinear, so early gains to stat experience were contributing relatively larger boosts to stats. 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 1000² = 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 223, 200, 200, 187, 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 down to an integer EV; but you also have to insert padding bytes, and don't corrupt the stat values (they also use `bigdw`), and do this for six stats 210 times.
+
+I advise you to learn a scripting language like Python, Perl, Ruby, etc, and write a program to automatically process the file.
+
+
+## 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 Attack. Follow [this tutorial](Add-different-kinds-of-new-items) to add a new item.
+
+First, add the essential data. Replace `ITEM_19` with `ZINC`; give it a name, description, and attributes (`9800, HELD_NONE, 0, CANT_SELECT, ITEM, ITEMMENU_PARTY, ITEMMENU_NOUSE`); give it `VitaminEffect`; and remove `ITEM_19` from `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.
diff --git a/Tutorials.md b/Tutorials.md
index b638202..4273509 100644
--- a/Tutorials.md
+++ b/Tutorials.md
@@ -56,6 +56,7 @@ Tutorials may use diff syntax to show edits:
**Features from later generations:**
- [Physical/Special split](Physical-Special-split)
+- [Replace stat experience with EVs](Replace-stat-experience-with-EVs)
- [Don't gain experience at level 100](Don't-gain-experience-at-level-100)
- [Infinitely reusable TMs](Infinitely-reusable-TMs)
- [Automatically reuse Repel](Automatically-reuse-Repel)
@@ -87,7 +88,6 @@ Tutorials may use diff syntax to show edits:
- Implement dynamic overhead+underfoot bridges
- Pan the camera for cutscenes by making the player invisible
- Gain experience from catching Pokémon
-- Replace stat experience with a modern EV system
- TM/HM item balls say the move name
- X Spcl.Def
- Nuzlocke mode (an in-game enforced [Nuzlocke Challenge](https://bulbapedia.bulbagarden.net/wiki/Nuzlocke_Challenge))
diff --git a/screenshots/zinc.png b/screenshots/zinc.png
new file mode 100644
index 0000000..0430309
--- /dev/null
+++ b/screenshots/zinc.png
Binary files differ