How to double experience gains in pokecrystal?

Started by Grate_Oracle_Lewot, April 26, 2022, 09:26:22 PM

Previous topic - Next topic

Grate_Oracle_Lewot

Hey peeps. I'm working on a pokecrystal-based hack, and I simply want to double all experience gains.

Github's disassembly and tutorials make some things easy enough to figure out, but I don't really understand the actual coding language being used, so despite doubling being such a simple operation, I can't quite work out how to do it. I can see that in engine/battle/core.asm, there's the "GiveExperiencePoints" part, which looks like all this:

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

.loop
ld hl, MON_HP
add hl, bc
ld a, [hli]
or [hl]
jp z, .next_mon ; fainted

push bc
ld hl, wBattleParticipantsNotFainted
ld a, [wCurPartyMon]
ld c, a
ld b, CHECK_FLAG
ld d, 0
predef SmallFarFlagAction
ld a, c
and a
pop bc
jp z, .next_mon

; 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
pop bc
ld hl, MON_LEVEL
add hl, bc
ld a, [hl]
cp MAX_LEVEL
jp nc, .next_mon
push bc
xor a
ldh [hMultiplicand + 0], a
ldh [hMultiplicand + 1], a
ld a, [wEnemyMonBaseExp]
ldh [hMultiplicand + 2], a
ld a, [wEnemyMonLevel]
ldh [hMultiplier], a
call Multiply
ld a, 7
ldh [hDivisor], a
ld b, 4
call Divide
; Boost Experience for traded Pokemon
pop bc
ld hl, MON_ID
add hl, bc
ld a, [wPlayerID]
cp [hl]
jr nz, .boosted
inc hl
ld a, [wPlayerID + 1]
cp [hl]
ld a, 0
jr z, .no_boost

.boosted
call BoostExp
ld a, 1

.no_boost
; Boost experience for a Trainer Battle
ld [wStringBuffer2 + 2], a
ld a, [wBattleMode]
dec a
call nz, BoostExp
; Boost experience for Lucky Egg
push bc
ld a, MON_ITEM
call GetPartyParamLocation
ld a, [hl]
cp LUCKY_EGG
call z, BoostExp
ldh a, [hQuotient + 3]
ld [wStringBuffer2 + 1], a
ldh a, [hQuotient + 2]
ld [wStringBuffer2], a
ld a, [wCurPartyMon]
ld hl, wPartyMonNicknames
call GetNickname
ld hl, Text_MonGainedExpPoint
call BattleTextbox
ld a, [wStringBuffer2 + 1]
ldh [hQuotient + 3], a
ld a, [wStringBuffer2]
ldh [hQuotient + 2], a
pop bc
call AnimateExpBar
push bc
call LoadTilemapToTempTilemap
pop bc
ld hl, MON_EXP + 2
add hl, bc
ld d, [hl]
ldh a, [hQuotient + 3]
add d
ld [hld], a
ld d, [hl]
ldh a, [hQuotient + 2]
adc d
ld [hl], a
jr nc, .no_exp_overflow
dec hl
inc [hl]
jr nz, .no_exp_overflow
ld a, $ff
ld [hli], a
ld [hli], a
ld [hl], a

.no_exp_overflow
ld a, [wCurPartyMon]
ld e, a
ld d, 0
ld hl, wPartySpecies
add hl, de
ld a, [hl]
ld [wCurSpecies], a
call GetBaseData
push bc
ld d, MAX_LEVEL
callfar CalcExpAtLevel
pop bc
ld hl, MON_EXP + 2
add hl, bc
push bc
ldh a, [hQuotient + 1]
ld b, a
ldh a, [hQuotient + 2]
ld c, a
ldh a, [hQuotient + 3]
ld d, a
ld a, [hld]
sub d
ld a, [hld]
sbc c
ld a, [hl]
sbc b
jr c, .not_max_exp
ld a, b
ld [hli], a
ld a, c
ld [hli], a
ld a, d
ld [hld], a

.not_max_exp
; Check if the mon leveled up
xor a ; PARTYMON
ld [wMonType], a
predef CopyMonToTempMon
callfar CalcLevel
pop bc
ld hl, MON_LEVEL
add hl, bc
ld a, [hl]
cp MAX_LEVEL
jp nc, .next_mon
cp d
jp z, .next_mon
; <NICKNAME> grew to level ##!
ld [wTempLevel], a
ld a, [wCurPartyLevel]
push af
ld a, d
ld [wCurPartyLevel], a
ld [hl], a
ld hl, MON_SPECIES
add hl, bc
ld a, [hl]
ld [wCurSpecies], a
ld [wTempSpecies], a ; unused?
call GetBaseData
ld hl, MON_MAXHP + 1
add hl, bc
ld a, [hld]
ld e, a
ld d, [hl]
push de
ld hl, MON_MAXHP
add hl, bc
ld d, h
ld e, l
ld hl, MON_STAT_EXP - 1
add hl, bc
push bc
ld b, TRUE
predef CalcMonStats
pop bc
pop de
ld hl, MON_MAXHP + 1
add hl, bc
ld a, [hld]
sub e
ld e, a
ld a, [hl]
sbc d
ld d, a
dec hl
ld a, [hl]
add e
ld [hld], a
ld a, [hl]
adc d
ld [hl], a
ld a, [wCurBattleMon]
ld d, a
ld a, [wCurPartyMon]
cp d
jr nz, .skip_active_mon_update
ld de, wBattleMonHP
ld a, [hli]
ld [de], a
inc de
ld a, [hli]
ld [de], a
ld de, wBattleMonMaxHP
push bc
ld bc, PARTYMON_STRUCT_LENGTH - MON_MAXHP
call CopyBytes
pop bc
ld hl, MON_LEVEL
add hl, bc
ld a, [hl]
ld [wBattleMonLevel], a
ld a, [wPlayerSubStatus5]
bit SUBSTATUS_TRANSFORMED, a
jr nz, .transformed
ld hl, MON_ATK
add hl, bc
ld de, wPlayerStats
ld bc, PARTYMON_STRUCT_LENGTH - MON_ATK
call CopyBytes

.transformed
xor a ; FALSE
ld [wApplyStatLevelMultipliersToEnemy], a
call ApplyStatLevelMultiplierOnAllStats
callfar ApplyStatusEffectOnPlayerStats
callfar UpdatePlayerHUD
call EmptyBattleTextbox
call LoadTilemapToTempTilemap
ld a, $1
ldh [hBGMapMode], a

.skip_active_mon_update
farcall LevelUpHappinessMod
ld a, [wCurBattleMon]
ld b, a
ld a, [wCurPartyMon]
cp b
jr z, .skip_exp_bar_animation
ld de, SFX_HIT_END_OF_EXP_BAR
call PlaySFX
call WaitSFX
ld hl, BattleText_StringBuffer1GrewToLevel
call StdBattleTextbox
call LoadTilemapToTempTilemap

.skip_exp_bar_animation
xor a ; PARTYMON
ld [wMonType], a
predef CopyMonToTempMon
hlcoord 9, 0
ld b, 10
ld c, 9
call Textbox
hlcoord 11, 1
ld bc, 4
predef PrintTempMonStats
ld c, 30
call DelayFrames
call WaitPressAorB_BlinkCursor
call SafeLoadTempTilemapToTilemap
xor a ; PARTYMON
ld [wMonType], a
ld a, [wCurSpecies]
ld [wTempSpecies], a ; unused?
ld a, [wCurPartyLevel]
push af
ld c, a
ld a, [wTempLevel]
ld b, a

.level_loop
inc b
ld a, b
ld [wCurPartyLevel], a
push bc
predef LearnLevelMoves
pop bc
ld a, b
cp c
jr nz, .level_loop
pop af
ld [wCurPartyLevel], a
ld hl, wEvolvableFlags
ld a, [wCurPartyMon]
ld c, a
ld b, SET_FLAG
predef SmallFarFlagAction
pop af
ld [wCurPartyLevel], a

.next_mon
ld a, [wPartyCount]
ld b, a
ld a, [wCurPartyMon]
inc a
cp b
jr z, .done
ld [wCurPartyMon], a
ld a, MON_SPECIES
call GetPartyParamLocation
ld b, h
ld c, l
jp .loop

.done
jp ResetBattleParticipants

.EvenlyDivideExpAmongParticipants:
; count number of battle participants
ld a, [wBattleParticipantsNotFainted]
ld b, a
ld c, PARTY_LENGTH
ld d, 0
.count_loop
push bc
push de
ld a, [wPartyCount]
cp c
jr c, .no_mon
ld a, c
dec a
ld hl, wPartyMon1Level
call GetPartyLocation
ld a, [hl]
.no_mon
cp MAX_LEVEL
pop de
pop bc
jr nz, .gains_exp
srl b
ld a, d
jr .no_exp
.gains_exp
xor a
srl b
adc d
ld d, a
.no_exp
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


And right under that, we have this "BoostExp" function:

BoostExp:
; Multiply experience by 1.5x
push bc
; load experience value
ldh a, [hProduct + 2]
ld b, a
ldh a, [hProduct + 3]
ld c, a
; halve it
srl b
rr c
; add it back to the whole exp value
add c
ldh [hProduct + 3], a
ldh a, [hProduct + 2]
adc b
ldh [hProduct + 2], a
pop bc
ret


...which boosts experience by 1.5 times under certain conditions. I could potentially hijack that x1.5, but I'd really rather do a x2, and I still want the x1.5 to apply afterward (if its conditions are met). Similarly, I could just call the GiveExperiencePoints function twice, but that would look stupid because it would go through the whole "X gained Y experience! X leveled up!" etc.etc. and then do so again a second time. I just don't know what all this "ld a" and "pop bc" and all that jazz means, and I imagine it's very easy to screw up if I mess with it.

Anybody familiar with pokecrystal?

KingMike

The "coding language" is Z80 assembly (the CPU the GB/GBC uses. The CPU is actually a unique variant.)
"My watch says 30 chickens" Google, 2018

thunderdisk

Pokecrystal's wiki has a bunch of links that can help you learn to program for the gameboy.

To double a value you might bitshift it to the left by one.

FAST6191

As above this is assembly code.

Two main approaches to this.
1) Find whatever table lists experience given and go to town.

2) The one you appear to be heading towards wherein the calculation is subverted.

Indeed the 1.5x (I presume that is for traded/rested/welcome back to the game) is somewhat close to what you are doing.

I don't know what we have for 8080/z80 tutorials these days. You might find a vintage tutorial out there.
Assembly though is basically a long list of simple instructions where other coding languages aim to abstract more and more of that away for you, and handle memory more easily.

Anyway the general gist of the 1.5x is after a check to see if the effect is in play then what it does is takes it, shifts it (shifting in binary works much like it does in decimal in that you shift the "decimal" point around, albeit for binary it is multiplying (and dividing, give or take remainder issues) by two depending upon the direction rather than 10 like the schoolboy maths trick.
In this case it takes the original experience (call it A), halves it with a shift (call it B if you like), now A+B== 1.5A give or take said remainder problem. https://gbdev.io/pandocs/CPU_Instruction_Set.html#rotate-and-shift-instructions would note SRL is short for shift right logical.
If you wanted to turn that into 2x what you would actually do is NOP (short for no operation, basically the CPU twiddles its thumbs for an instruction) on the shift and it would add it back in.
What you would want to do here however is whenever it generates (looks up presumably) the value for the mons you knocked out that you do your own shift (albeit leftwards) to double that. I don't know if there will be enough space in there without looking or if there are instructions you might be able to lose/optimise without properly going through that. Possible to jump to somewhere and jump back though in the same bank.

Grate_Oracle_Lewot

Quote from: FAST6191 on April 27, 2022, 07:04:30 AM
1) Find whatever table lists experience given and go to town.

Yeah, each Pokemon has a base stats file, which includes base experience yield, but the problem is those can only go up to 255, and some of them are already close to that, so I can't double them there without figuring out how to add more storage for them or whatever.

Quote from: FAST6191 on April 27, 2022, 07:04:30 AM
Anyway the general gist of the 1.5x is after a check to see if the effect is in play then what it does is takes it, shifts it (shifting in binary works much like it does in decimal in that you shift the "decimal" point around, albeit for binary it is multiplying (and dividing, give or take remainder issues) by two depending upon the direction rather than 10 like the schoolboy maths trick.
In this case it takes the original experience (call it A), halves it with a shift (call it B if you like), now A+B== 1.5A give or take said remainder problem. https://gbdev.io/pandocs/CPU_Instruction_Set.html#rotate-and-shift-instructions would note SRL is short for shift right logical.
If you wanted to turn that into 2x what you would actually do is NOP (short for no operation, basically the CPU twiddles its thumbs for an instruction) on the shift and it would add it back in.
What you would want to do here however is whenever it generates (looks up presumably) the value for the mons you knocked out that you do your own shift (albeit leftwards) to double that. I don't know if there will be enough space in there without looking or if there are instructions you might be able to lose/optimise without properly going through that. Possible to jump to somewhere and jump back though in the same bank.

So if SRL is "shift right logical," then shifting left would be something like SLL? I can probably figure it out from there. I don't know if there will be space for it either, but I'll find out. Thanks!