Experience System & Exp. All Enhancements (Single message, etc) - pret/pokered GitHub Wiki

This is a transcription of a tutorial suloku posted on PasteBin. View that for a more thorough explanation of the Exp. All's issues, including examples.

In RBY, the Exp. All is kind of...silly. Here's how it works.

  • Total experience is divided by two.
  • 1/2 goes to the pokémon that participated in the battle.
  • 1/2 is to be shared among the whole party, which is then divided amongst them.
  • Traded EXP boost is applied (so any traded Pokemon gets an additional 1.5 bonus)

Due to the way this works, when shared amongst a party of 6, Pokemon actually end up getting 1/12 of the exp gained. This makes the Exp. All a largely useless item outside of exploiting division errors to get 0 ATK Stat Exp on a Pokemon (optimising for confusion damage), or getting maximum Stat Exp on a low-level Pokemon, both of which take an extremely long time to perform anyway. With a use case this narrow, it's no wonder you're here reading this tutorial!

Implementing these fixes does have some minor side effects, which will be covered as we go.

Fixing the division issue

First, we should look at the exp division. We will fix this with a very simple change in engine/battle/experience.asm, by removing a call to DivideExpDataByNumMonsGainingExp, which is what divides the experience amongst the party members.

GainExperience:
	ld a, [wLinkState]
	cp LINK_STATE_BATTLING
	ret z ; return if link battle
-	call DivideExpDataByNumMonsGainingExp ;--> we are deleting this line!
	ld hl, wPartyMon1
	xor a
	ld [wWhichPokemon], a

And because that function now goes unused, you can delete it. It should be around line 303:

; divide enemy base stats, catch rate, and base exp by the number of mons gaining exp
-DivideExpDataByNumMonsGainingExp:
-	ld a, [wPartyGainExpFlags]
-	ld b, a
-	xor a
-	ld c, $8
-	ld d, $0
-.countSetBitsLoop ; loop to count set bits in wPartyGainExpFlags
-	xor a
-	srl b
-	adc d
-	ld d, a
-	dec c
-	jr nz, .countSetBitsLoop
-	cp $2
-	ret c ; return if only one mon is gaining exp
-	ld [wd11e], a ; store number of mons gaining exp
-	ld hl, wEnemyMonBaseStats
-	ld c, wEnemyMonBaseExp + 1 - wEnemyMonBaseStats
-.divideLoop
-	xor a
-	ldh [hDividend], a
-	ld a, [hl]
-	ldh [hDividend + 1], a
-	ld a, [wd11e]
-	ldh [hDivisor], a
-	ld b, $2
-	call Divide ; divide value by number of mons gaining exp
-	ldh a, [hQuotient + 3]
-	ld [hli], a
-	dec c
--	jr nz, .divideLoop
-	ret

This results in a side effect, though: All participants will get 100% EXP when not using the EXP. All, which is actually better than XY's Experience System.

Implementing just this results in two issues:

  1. Using the Exp. All, you will get an Exp message for every participant in the battle, plus 1 message per team member. This is extremely slow and makes using it tiresome, so two actions should be done: 1.1) Maintain the messages for participating pokémon 1.2) Show only one generic message for the whole party

  2. 1 Experience Point is lost on odd exp yields since halved EXP is rounded down, so if a Pokémon yields 81 exp, participants will get 40 + 40 Exp foro a total of 80 exp. This is a minor inconvenience, but can be solved by rounding the halved EXP up instead of down in engine\battle\core.asm.

Both of these are perfectly solvable, so let's patch them up.

Exp. All Single Message

Anyone who's used the Exp. All has experienced the agony as it tells you how strong your entire party has become. Let's remove that! If you're not interested in using the modern experience system, you can walk away with just this implemented.

Let's start from the top of GainExperience. Implement as follows;

GainExperience:
	ld a, [wLinkState]
	cp LINK_STATE_BATTLING
	ret z ; return if link battle
	call DivideExpDataByNumMonsGainingExp ; if you're using the gen 6 exp system, remove this too
+	ld a, [wBoostExpByExpAll] ;load in a if the EXP All is being used
+	ld hl, WithExpAllText ; this is preparing the text to show
+	and a ;check wBoostExpByExpAll value
+	jr z, .skipExpAll ; if wBoostExpByExpAll is zero, we are not using it, so we don't show anything and keep going on
+	call PrintText ; if the code reaches this point it means we have the Exp.All, so show the message
+.skipExpAll
	ld hl, wPartyMon1
	xor a
-	ld [wWhichPokemon], a
-	ld hl, wPartyMon1
-	xor a
	ld [wWhichPokemon], a
.partyMonLoop ; loop over each mon and add gained exp

This sets up the text we want to use later. You'll notice the "with Exp. All" text is being repurposed, we'll get to that in a second!

Now go to GainExperience.next2. Ctrl+F ".next2" gets you there.

.next2
    push hl
    ld a, [wWhichPokemon]
    ld hl, wPartyMonNicks
    call GetPartyMonName
-   ld hl, GainedText
+   ld a, [wBoostExpByExpAll] ; get using ExpAll flag
+   and a ; check the flag
+   jr nz, .skipExpText ; if there's EXP. all, skip showing any text
+   ld hl, GainedText ;there's no EXP. all, load the text to show
    call PrintText
+.skipExpText
    xor a ; PLAYER_PARTY_DATA
    ld [wMonDataLocation], a

This checks if the Exp. All is active, and sets up the usual "gained xxxx Exp!" message in the usual way.

We're going to repurpose some text here. Go to data\text\text_2.asm and make the following changes;

_WithExpAllText::
	text "with EXP.ALL,"
	cont "@"
	text_end

As-is, this will make your text look a bit weird, so let's repurpose it to look like this:

_WithExpAllText::
+	text "Party gained"
+	next "@"
	text_end

Now your Exp gains will look like this: "PIKACHU gained" "xxxx EXP. Points!" "Party gained" "xxxx EXP. Points!"

However, you will notice a couple of seconds of lag after that last message has been put out. This is pretty much unavoidable: What's happening there, is the game is calculating the stat gains from the experience given. It does still end up being faster than the original Exp. All, but it's still quite jarring.