Generation 6 Experience System - pret/pokecrystal GitHub Wiki
In Pokémon X/Y the experience system received some changes, giving 100% of the total experience earned in battle to all participant Pokémon. The Exp. Share was also reworked to be a Key Item that can be turned on or off, granting to all non-fainted Pokémon who didn't participate in battle 50% of the total experience individually.
The goal of this tutorial is to teach you how to upgrade the experience system to behave in a similar way.
NOTE: As a side effect, both participant and non-participant Pokémon will also receive 100% of the total Stat Experience (a.k.a Stat Exp., the predecessor of Effort Values in generation 2) whenever an enemy faints.
Without further ado, let's start!
Before tackling the experience system, we must first change how the Exp. Share item works. In the first part of the tutorial we'll transform it into a Key Item that, when turned on, will tell the responsible function to give 50% of total experience to non-participant Pokémon.
Before modifying how the Exp. Share works, we have to create a WRAM label to store the current state of the toggle. For that, let's edit ram/wram.asm:
wHallOfFameCount:: db
- ds 1
+wExpShareToggle:: db
wTradeFlags:: flag_array NUM_NPC_TRADES
ds 1
wMooMooBerries:: db
The reason we chose this specific spot is because it's within a section which contains information that's preserved when you save your game; in other words, if we decide to reboot the game the toggle's state won't change from the last time we saved.
The next step is to transform the Exp. Share into a Key Item that can be toggled when used, just like in generation 6 and 7. For this, we'll edit the item's attributes, description and effect when used. For this, we'll edit various files, starting with data/items/attributes.asm, giving it attributes similar to a Key Item:
; EXP_SHARE
- item_attribute 3000, HELD_NONE, 0, CANT_SELECT, ITEM, ITEMMENU_NOUSE, ITEMMENU_NOUSE
+ item_attribute 0, HELD_NONE, 0, CANT_SELECT | CANT_TOSS, KEY_ITEM, ITEMMENU_CURRENT, ITEMMENU_NOUSE
NOTE: If you don't know how the attributes' values work or what they mean, please refer to this tutorial about adding a new item for more information.
The next file is data/items/descriptions.asm, where we'll update the item's description to clarify it's no longer holdable:
ExpShareDesc:
db "Shares battle EXP."
- next "Points. (HOLD)@"
+ next "Points.@"
We also must edit engine/items/item_effects.asm, where it defines and assigns the item effects when used from the bag in the overworld/during battle. Let's create ExpShareEffect
to update the Exp. Share's toggle value whenever is used, and give it to the Exp. Share item:
ItemEffects:
; entries correspond to item ids (see constants/item_constants.asm)
table_width 2, ItemEffects
...
- dw NoEffect ; EXP_SHARE
+ dw ExpShareEffect ; EXP_SHARE
...
assert_table_length ITEM_B3
...
-LoadHPIntoBuffer3: ; unreferenced
- ld a, d
- ld [wHPBuffer3 + 1], a
- ld a, e
- ld [wHPBuffer3], a
- ret
-
-LoadHPFromBuffer3: ; unreferenced
- ld a, [wHPBuffer3 + 1]
- ld d, a
- ld a, [wHPBuffer3]
- ld e, a
- ret
+ExpShareEffect:
+ ld a, [wExpShareToggle]
+ xor 1
+ ld [wExpShareToggle], a
+ and a
+ ld hl, ExpShareToggleOn
+ jp nz, PrintText
+
+ ld hl, ExpShareToggleOff
+ jp PrintText
...
-ItemGotOnText: ; unreferenced
- text_far _ItemGotOnText
- text_end
-ItemGotOffText: ; unreferenced
- text_far _ItemGotOffText
- text_end
+ExpShareToggleOff:
+ text_far _ExpShareToggleOff
+ text_end
+ExpShareToggleOn:
+ text_far _ExpShareToggleOn
+ text_end
We deleted unreferenced functions to save space while at the same time adding our new effect alongside the text commands necessary to display text whenever it's used.
Speaking of text, let's go to data/text/common_3.asm and replace the now unused _ItemGotOnText
and _ItemGotOffText
with two new texts for when the Exp. Share is turned on/off:
-_ItemGotOnText::
- text "<PLAYER> got on the@"
- text_low
- text_ram wStringBuffer2
- text "."
- prompt
-
-_ItemGotOffText::
- text "<PLAYER> got off@"
- text_low
- text "the @"
- text_ram wStringBuffer2
- text "."
- prompt
+_ExpShareToggleOn::
+ text "The EXP.SHARE was"
+ line "turned on."
+ prompt
+
+_ExpShareToggleOff::
+ text "The EXP.SHARE was"
+ line "turned off."
+ prompt
There are two instances in the game where we can get the Exp. Share item: when giving the Red Scale to Mr. Pokémon and as a 2nd place's prize from the Lottery Corner. Now that it's a Key Item, we only need to get it once, so we'll replace the Lottery Corner prize with a normal item.
In this example, we'll replace it with a Lucky Egg. Edit maps/RadioTower1F.asm:
RadioTower1FLuckyNumberManScript:
...
.SecondPlace:
writetext RadioTower1FLuckyNumberManOkayMatchText
playsound SFX_2ND_PLACE
waitsfx
promptbutton
- giveitem EXP_SHARE
+ giveitem LUCKY_EGG
iffalse .BagFull
itemnotify
setflag ENGINE_LUCKY_NUMBER_SHOW
sjump .GameOver
...
RadioTower1FLuckyNumberManOkayMatchText:
text "Hey! You've"
line "matched the last"
cont "three numbers!"
para "You've won second"
- line "prize, an EXP."
- cont "SHARE!"
+ line "prize, a LUCKY"
+ cont "EGG!"
done
Our new Exp. Share is working, but the toggle itself isn't doing anything. In this second part of the tutorial we'll make it work and also rework the experience system to give 100% of the total experience to each participant Pokémon (and 50% experience to each non-participants if the Exp. Share is on).
We'll perform the first step in engine/battle/core.asm by editing how to handle the gained experience after defeating an enemy. For that let's go and edit UpdateBattleStateAndExperienceAfterEnemyFaint
:
UpdateBattleStateAndExperienceAfterEnemyFaint:
...
.player_mon_did_not_faint
...
ld a, [wBattleResult]
and BATTLERESULT_BITMASK
ld [wBattleResult], a ; WIN
- call IsAnyMonHoldingExpShare
- jr z, .skip_exp
- ld hl, wEnemyMonBaseStats
- ld b, wEnemyMonEnd - wEnemyMonBaseStats
-.loop
- srl [hl]
- inc hl
- dec b
- jr nz, .loop
-
-.skip_exp
- ld hl, wEnemyMonBaseStats
- ld de, wBackupEnemyMonBaseStats
- ld bc, wEnemyMonEnd - wEnemyMonBaseStats
- call CopyBytes
- xor a
- ld [wGivingExperienceToExpShareHolders], a
- call GiveExperiencePoints
- call IsAnyMonHoldingExpShare
- ret z
+ ; Preserve bits of non-fainted participants
+ ld a, [wBattleParticipantsNotFainted]
+ ld d, a
+ push de
+ call GiveExperiencePoints
+ pop de
+ ; If Exp. Share is ON, give 50% EXP to non-participants
+ ld a, [wExpShareToggle]
+ and a
+ ret z
+ ld hl, wEnemyMonBaseExp
+ srl [hl]
ld a, [wBattleParticipantsNotFainted]
push af
ld a, d
+ xor %00111111
ld [wBattleParticipantsNotFainted], a
- ld hl, wBackupEnemyMonBaseStats
- ld de, wEnemyMonBaseStats
- ld bc, wEnemyMonEnd - wEnemyMonBaseStats
- call CopyBytes
- ld a, $1
- ld [wGivingExperienceToExpShareHolders], a
call GiveExperiencePoints
pop af
ld [wBattleParticipantsNotFainted], a
ret
Originally the code used to check if at least one of our Pokémon had the Exp. Share equipped (done by IsAnyMonHoldingExpShare
) and, if that was the case, halve both the total experience and the Stat Exp.; the first half was divided among participant Pokémon and the other half among Exp. Share holders.
Now the code will try to divide 100% of the total experience among each participant and, if the Exp. Share is on, halve the total experience and distribute it among non-participants. You might've also noticed that we're preserving the value of wBattleParticipantsNotFainted
in register d
, this is because it gets overwritten at some point by GiveExperiencePoints
and we need the original value to invert its bits and give 50% of the experience to the Pokémon who didn't participate in battle.
There's still one thing, though. If you paid attention, I said the code still tries to divide the experience among participants and/or non-participants and we need to change this behavior. For this we can simply delete the call to .EvenlyDivideExpAmongParticipants
in the same file:
GiveExperiencePoints:
; Give experience.
; Don't give experience if linked or in the Battle Tower.
ld a, [wLinkMode]
and a
ret nz
ld a, [wInBattleTowerBattle]
bit 0, a
ret nz
- call .EvenlyDivideExpAmongParticipants
xor a
ld [wCurPartyMon], a
ld bc, wPartyMon1Species
...
And since .EvenlyDivideExpAmongParticipants
is now unused, we can also safely delete the local function, which is inside GiveExperiencePoints
:
-.EvenlyDivideExpAmongParticipants:
-; count number of battle participants
- ld a, [wBattleParticipantsNotFainted]
- ld b, a
- ld c, PARTY_LENGTH
- ld d, 0
-.count_loop
- xor a
- srl b
- adc d
- ld d, a
- dec c
- jr nz, .count_loop
- cp 2
- ret c
-
- ld [wTempByteValue], a
- ld hl, wEnemyMonBaseStats
- ld c, wEnemyMonEnd - wEnemyMonBaseStats
-.base_stat_division_loop
- 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
- ret
The only instance left where IsAnyMonHoldingExpShare
is being used is in PlayVictoryMusic
, which was appropriate since the first function not only checks if at least one Pokémon has the Exp. Share equipped, but also if they're alive; if the check is passed, then we won the battle, playing the victory music. Now that the Exp. Share is a toggleable Key item it can't be used to reliably tell if we have at least one Pokémon alive. Delete its usage in PlayVictoryMusic
, located in engine/battle/core.asm:
PlayVictoryMusic:
push de
ld de, MUSIC_NONE
call PlayMusic
call DelayFrame
ld de, MUSIC_WILD_VICTORY
ld a, [wBattleMode]
dec a
jr nz, .trainer_victory
- push de
- call IsAnyMonHoldingExpShare
- pop de
- jr nz, .play_music
ld hl, wPayDayMoney
...
Finally, we can safely delete the IsAnyMonHoldingExpShare
function itself located in the same file, since it's now completely unused:
-IsAnyMonHoldingExpShare:
- ld a, [wPartyCount]
- ld b, a
- ld hl, wPartyMon1
- ld c, 1
- ld d, 0
-.loop
- push hl
- push bc
- ld bc, MON_HP
- add hl, bc
- ld a, [hli]
- or [hl]
- pop bc
- pop hl
- jr z, .next
-
- push hl
- push bc
- ld bc, MON_ITEM
- add hl, bc
- pop bc
- ld a, [hl]
- pop hl
-
- cp EXP_SHARE
- jr nz, .next
- ld a, d
- or c
- ld d, a
-
-.next
- sla c
- push de
- ld de, PARTYMON_STRUCT_LENGTH
- add hl, de
- pop de
- dec b
- jr nz, .loop
-
- ld a, d
- ld e, 0
- ld b, PARTY_LENGTH
-.loop2
- srl a
- jr nc, .okay
- inc e
-
-.okay
- dec b
- jr nz, .loop2
- ld a, e
- and a
- ret
With all the stuff we changed/deleted, some WRAM labels became unused and can safely be replaced with empty allocated bytes. For this, go to ram/wram.asm:
wPlayerFutureSightCount:: db
wEnemyFutureSightCount:: db
-wGivingExperienceToExpShareHolders:: db
-
-wBackupEnemyMonBaseStats:: ds NUM_EXP_STATS
-wBackupEnemyMonCatchRate:: db
-wBackupEnemyMonBaseExp:: db
+ ds 8
wPlayerFutureSightDamage:: dw
wEnemyFutureSightDamage:: dw
By allocating empty space in memory we know there are empty unused bytes we can replace in the future for new WRAM labels. Also we avoid accidentally shifting memory addresses, probably rendering our save file useless. (If you don't know how ds
, db
or dw
work, please refer to the RGBDS's assembly syntax documentation).
And that's it! It's been a long tutorial, but we're finally done reworking the experience system along with the Exp. Share item to work like in generation 6. Remember to balance your romhack appropriately!