• Our software update is now concluded. You will need to reset your password to log in. In order to do this, you will have to click "Log in" in the top right corner and then "Forgot your password?".
  • Welcome to PokéCommunity! Register now and join one of the best fan communities on the 'net to talk Pokémon and more! We are not affiliated with The Pokémon Company or Nintendo.

Soft EXP Level Caps

146
Posts
16
Years
    • Age 26
    • Seen Apr 29, 2024
    If you've implemented this code before 6/8/2020, please, reimplement it! There were quite a few bugs and things not accounted for when I originally posted this, but everything should be 100% good to go now. My apologies. I hope this isn't much of an inconvenience for you.

    Ever since playing Pokemon Clover, I have been absolutely in love with how they handled difficulty. If you haven't played it, it works like this: Your experience gain slows down as you hit a soft level cap. For example, if you want a player to be around level 13 for the first gym, once a player hits level 13, their experience gain will slow down further and further as the player's level gets higher. This promotes less grinding and more strategy, which I think is a great way to design a Pokemon hack.
    So, after finding some unofficial documentation on Pastebin, I decided to try to implement it myself, and to my surprise, it wasn't very hard.

    First, go into include\event_data.h and add this define above the includes:
    Code:
    #define NUM_SOFT_CAPS 8

    At the bottom of the file (before #endif, of course), add these lines:
    Code:
    extern const u16 sLevelCapFlags[NUM_SOFT_CAPS];
    extern const u16 sLevelCaps[NUM_SOFT_CAPS];
    event_data.h is used for these, as both src\battle_script_commands.c and src\daycare.c both include it, and it includes include\constants\flags.h, which we need for one of our arrays when we define it.

    Now, go into src\battle_script_commands.c and search for this line:
    Code:
    static void Cmd_trainerslideout(void);
    All of our variables for this will go between the end of the list of static void Cmd_whatever and void (* const gBattleScriptingCommandsTable[])(void) =, so this may not be the end of the list of methods if you're using pokemon_expansion or some other modification.

    Under that line, add a new array of flags that will denote your level caps under that line. For this tutorial, I'll be using badge flags to denote my caps (meaning I'll have a soft cap for every gym leader), but you can define whatever flags you so desire here.
    Code:
    const u16 sLevelCapFlags[NUM_SOFT_CAPS] =
    {
        FLAG_BADGE01_GET, FLAG_BADGE02_GET, FLAG_BADGE03_GET, FLAG_BADGE04_GET,
        FLAG_BADGE05_GET, FLAG_BADGE06_GET, FLAG_BADGE07_GET, FLAG_BADGE08_GET,
    };
    If you don't want to use badges, or want to use more flags than just badges, check out include\constants\flags.h for a list of predefined flags that are available for use.

    Under your newly defined array of flags, add these lines:
    Code:
    const u16 sLevelCaps[NUM_SOFT_CAPS] = { 10, 20, 30, 40, 50, 60, 70, 80 };
    const double sLevelCapReduction[7] = { .5, .33, .25, .20, .15, .10, .05 };
    const double sRelativePartyScaling[27] =
    {
        3.00, 2.75, 2.50, 2.33, 2.25,
        2.00, 1.80, 1.70, 1.60, 1.50,
        1.40, 1.30, 1.20, 1.10, 1.00,
        0.90, 0.80, 0.75, 0.66, 0.50,
        0.40, 0.33, 0.25, 0.20, 0.15,
        0.10, 0.05,
    };
    A quick explanation of everything:
    sLevelCaps is going to be whatever you want your level cap to be. As of right now, everything is set up in increments of 10--this was done purely for this example, and you should definitely change this to fit the difficulty curve of your hack. You'll want to make sure that sLevelCaps is the same length as your sLevelCapFlags array, as they need to be the same length for some code later on. So for example, if you have 13 custom flags, you'll want sLevelCaps to have 13 different values as well.
    sLevelCapReduction is used to scale experience gain more and more the further you get away from the cap. These values are taken directly from the first unofficial Clover documentation linked above.
    sRelativePartyScaling is another mechanic in Clover that allows lower leveled Pokemon in your party to gain more experience. For example, if the average level of a team is 45, and the Pokemon you are training is level 37, 37 - 45 = -8. Since arrays begin at 0, we then add 14 to this number (as 14 is the lowest negative value shown in the second unofficial Clover documentation) and are left with 6. sRelativePartyScaling[6] is 1.80, so total calculated experience will be multiplied by 1.8 for the underleveled Pokemon.

    Next, search for Cmd_getexp(void), and add these methods above it:
    Code:
    u8 GetTeamLevel(void)
    {
        u8 i;
        u16 partyLevel = 0;
        u16 threshold = 0;
    
        for (i = 0; i < PARTY_SIZE; i++)
        {
            if (GetMonData(&gPlayerParty[i], MON_DATA_SPECIES) != SPECIES_NONE)
                partyLevel += gPlayerParty[i].level;
            else
                break;
        }
        partyLevel /= i;
    
        threshold = partyLevel * .8;
        partyLevel = 0;
    
        for (i = 0; i < PARTY_SIZE; i++)
        {
            if (GetMonData(&gPlayerParty[i], MON_DATA_SPECIES) != SPECIES_NONE)
            {
                if (gPlayerParty[i].level >= threshold)
                    partyLevel += gPlayerParty[i].level;
            }
            else
                break;
        }
        partyLevel /= i;
    
        return partyLevel;
    }
    
    double GetPkmnExpMultiplier(u8 level)
    {
        u8 i;
        double lvlCapMultiplier = 1.0;
        u8 levelDiff;
        s8 avgDiff;
    
        // multiply the usual exp yield by the soft cap multiplier
        for (i = 0; i < NUM_SOFT_CAPS; i++)
        {
            if (!FlagGet(sLevelCapFlags[i]) && level >= sLevelCaps[i])
            {
                levelDiff = level - sLevelCaps[i];
                if (levelDiff > 6)
                    levelDiff = 6;
                lvlCapMultiplier = sLevelCapReduction[levelDiff];
                break;
            }
        }
    
        // multiply the usual exp yield by the party level multiplier
        avgDiff = level - GetTeamLevel();
    
        if (avgDiff >= 12)
            avgDiff = 12;
        else if (avgDiff <= -14)
            avgDiff = -14;
    
        avgDiff += 14;
    
        return lvlCapMultiplier * sRelativePartyScaling[avgDiff];
    }
    GetTeamLevel simply returns the average level of your party for the relative party scaling, while not including levels that are too low that can skew the average.
    GetPkmnExpMultiplier finds the level difference (if you're above the level cap, of course) and applies that reduction, then applies the relative party experience modifier, and returns these modifiers multiplied together.
    Big shoutouts to blakcat for his help with reworking this code to work properly in double-battles.

    Now, search for this if statement within case 2 of Cmd_getexp:
    Code:
    if (GetMonData(&gPlayerParty[gBattleStruct->expGetterMonId], MON_DATA_HP))

    Immediately after that if statement, add this line:
    Code:
    double expMultiplier = GetPkmnExpMultiplier(gPlayerParty[gBattleStruct->expGetterMonId].level);

    Then, replace these lines:
    Code:
    if (gBattleStruct->sentInPokes & 1)
        gBattleMoveDamage = *exp;
    else
        gBattleMoveDamage = 0;
    
    if (holdEffect == HOLD_EFFECT_EXP_SHARE)
        gBattleMoveDamage += gExpShareExp;

    With these lines:
    Code:
    if (gBattleStruct->sentInPokes & 1)
        gBattleMoveDamage = *exp * expMultiplier;
    else
        gBattleMoveDamage = 0;
    
    if (holdEffect == HOLD_EFFECT_EXP_SHARE)
        gBattleMoveDamage += gExpShareExp * expMultiplier;
    Which should apply your multiplier to your gained experience. With that, we are done with the battle experience gain system. Thanks again to blakcat for helping rework my original code.

    Now, to add functionality to the Day Care experience gain system. Open up src\daycare.c and look for this if statement in the TakeSelectedPokemonFromDaycare method:
    Code:
    if (GetMonData(&pokemon, MON_DATA_LEVEL) != MAX_LEVEL)

    We're going to completely replace this if statement with this new one:
    Code:
    if (GetMonData(&pokemon, MON_DATA_LEVEL) != MAX_LEVEL)
    {
        u8 level;
        u8 i;
        u8 cap;
    
        experience = GetMonData(&pokemon, MON_DATA_EXP) + daycareMon->steps;
        SetMonData(&pokemon, MON_DATA_EXP, &experience);
        level = GetLevelFromMonExp(&pokemon);
    
        for (i = 0; i < NUM_SOFT_CAPS; i++)
        {
            if (i <= 2)
                cap = sLevelCaps[i] / 2;
            else
                cap = sLevelCaps[i];
    
            if (!FlagGet(sLevelCapFlags[i]) && level >= cap)
            {
                u8 levelDiff;
                u32 newSteps;
    
                levelDiff = level - cap;
    
                newSteps = daycareMon->steps / (levelDiff + 1);
                experience = GetBoxMonData(&daycareMon->mon, MON_DATA_EXP) + newSteps;
    
                SetMonData(&pokemon, MON_DATA_EXP, &experience);
                break;
            }
        }
    
        ApplyDaycareExperience(&pokemon);
    }
    Thanks to defining the flag and level cap arrays as externs in the include\event_data.h file earlier, we do not need to redefine them here.

    And now, we're going to replace the entire GetLevelAfterDaycareSteps method with some very similar code:
    Code:
    static u8 GetLevelAfterDaycareSteps(struct BoxPokemon *mon, u32 steps)
    {
        struct BoxPokemon tempMon = *mon;
        u32 experience = GetBoxMonData(mon, MON_DATA_EXP) + steps;
        u8 i;
        u8 level;
        u8 cap;
    
        // set experience now to be able to get levelAfter
        SetBoxMonData(&tempMon, MON_DATA_EXP, &experience);
        level = GetLevelFromBoxMonExp(&tempMon);
    
        // loop through to check caps
        for (i = 0; i < NUM_SOFT_CAPS; i++)
        {
            if (i <= 2)
                cap = sLevelCaps[i] / 2;
            else
                cap = sLevelCaps[i];
    
            if (!FlagGet(sLevelCapFlags[i]) && level >= cap)
            {
                u8 levelDiff;
                u32 newSteps;
    
                levelDiff = level - cap;
    
                newSteps = steps / (levelDiff + 1);
                experience = GetBoxMonData(mon, MON_DATA_EXP) + newSteps;
    
                SetBoxMonData(&tempMon, MON_DATA_EXP, &experience);
                break;
            }
        }
    
        return GetLevelFromBoxMonExp(&tempMon);
    }
    Now, this is handled quite a bit differently than the battle system, so let me explain. The original Day Care gives your Pokemon one experience point for every step you take after dropping it off at the Day Care. I found that the best way to reduce experience gained from the Day Care was to simply take the total amount of steps and divide that number by the level difference plus one. After doing that, add it to the Day Care Pokemon's original experience from when it got dropped off, and then set the new experience.
    I tried making this work with the same level reduction curve that is used in src\battle_script_commands.c, but this seemed to work much better. As for dividing the level cap in half if i <= 2, in my testing I found that since earlier levels require a lot less experience to level up, the level caps should proc earlier than under a regular battle circumstance. This can be adjusted depending on what your level caps are/how many you have, I just found 1 or 2 to be a good cutoff point.

    And that's it!
    Your hack now has a soft cap experience gain system.
    Thanks to LuckyTyphlosion on Pastebin for posting their findings regarding the level cap system and how it works.
     
    Last edited:
    6
    Posts
    8
    Years
    • Seen Feb 12, 2024
    Tried adding this but could not find static const u16 sBadgeFlags[8] = in src/battle_script_commands.c
    Edit: I was told its in battle_setup.c now, are you sure this is still updated?
     
    Last edited:
    146
    Posts
    16
    Years
    • Age 26
    • Seen Apr 29, 2024
    Tried adding this but could not find static const u16 sBadgeFlags[8] = in src/battle_script_commands.c
    Edit: I was told its in battle_setup.c now, are you sure this is still updated?

    Oh my apologies, I didn't realize that this was defined by another addition I made. I will be updating the tutorial now. Thanks for pointing it out!

    EDIT: Tutorial is up-to-date now, thanks again.
     
    Last edited:
    146
    Posts
    16
    Years
    • Age 26
    • Seen Apr 29, 2024
    A thought I just had. This doesn't change daycare levelling does it?

    Sorry for it being a day late, but I just added Day Care scaling. Thanks again for helping point out my issues, I would have completely forgotten about Day Care if not for you, haha.
     
    55
    Posts
    14
    Years
    • Seen Feb 1, 2023
    Hi, PokemonCrazy, thanks for the awesome tut! Just wanted to point out 1 typo: in the level cap reduction list you have .5 instead of .05 --
    Code:
    const double sLevelCapReduction[7] = { .5, .33, .25, .20, .15, .10, .05 };
    would be correct (and I double-checked with the docs you posted).

    Thanks again :)
     
    146
    Posts
    16
    Years
    • Age 26
    • Seen Apr 29, 2024
    Hi, PokemonCrazy, thanks for the awesome tut! Just wanted to point out 1 typo: in the level cap reduction list you have .5 instead of .05 --
    Code:
    const double sLevelCapReduction[7] = { .5, .33, .25, .20, .15, .10, .05 };
    would be correct (and I double-checked with the docs you posted).

    Thanks again :)

    I meant to fix this in the last edit, my bad! Thanks for catching my mistake.
     
    55
    Posts
    14
    Years
    • Seen Feb 1, 2023
    I meant to fix this in the last edit, my bad! Thanks for catching my mistake.

    Thanks for the fast response!

    I have a question for you: I added this into my hack which also has Dizzy's battle engine v2. His engine seems to have added double wild battles, and when I beat them, my pokémon (of different levels) both receive the same amount of EXP (see my post in the battle engine thread for more details).

    It seems like Cmd_getexp only gets called 1 time and so the level multipliers are calculated based on the first pkmn instead of individually. To fix this, I broke out some of your code into a separate function:
    Code:
    static double getPkmnExpMultiplier(u8 level)
    {
        u8 i;
        double lvlCapMultiplier = 1.0;
        u8 levelDiff;
        s8 avgDiff;
    
        //multiply the usual exp yield by the soft cap multiplier
        for(i = 0; i < NUM_SOFT_CAPS; i++)
        {
            if(!FlagGet(sLevelCapFlags[i]) && level > sLevelCaps[i])
            {
                levelDiff = level - sLevelCaps[i];
                if(levelDiff > 7)
                    levelDiff = 7;
                lvlCapMultiplier = sLevelCapReduction[levelDiff];
            }
        }
    
        //multiply the usual exp yield by the party level multiplier
        avgDiff = level - GetTeamLevel();
    
        if(avgDiff >= MAX_LVL_DIFF_THRESHOLD)
            avgDiff = MAX_LVL_DIFF_THRESHOLD;
        else if(avgDiff <= MIN_LVL_DIFF_THRESHOLD)
            avgDiff = MIN_LVL_DIFF_THRESHOLD;
    
        avgDiff += (MIN_LVL_DIFF_THRESHOLD * -1);
    
        return lvlCapMultiplier * sRelativePartyScaling[avgDiff];
    }

    and defined the constants in battle_scripts.h:
    Code:
    // soft level cap constants
    #define NUM_SOFT_CAPS              8
    #define MAX_LVL_DIFF_THRESHOLD    12
    #define MIN_LVL_DIFF_THRESHOLD   -14

    and then I called it from the case 2 block: (search for this line: if (GetMonData(&gPlayerParty[gBattleStruct->expGetterMonId], MON_DATA_HP)))
    Code:
    if(GetMonData(&gPlayerParty[gBattleStruct->expGetterMonId], MON_DATA_HP))
    {
        double expMultiplier = getPkmnExpMultiplier(gPlayerParty[gBattleStruct->expGetterMonId].level);
    
        if(gBattleStruct->sentInPokes & 1)
            gBattleMoveDamage = *exp * expMultiplier;
        else
            gBattleMoveDamage = 0;
    
        if(holdEffect == HOLD_EFFECT_EXP_SHARE)
            gBattleMoveDamage += gExpShareExp * expMultiplier;
        if(holdEffect == HOLD_EFFECT_LUCKY_EGG)
            gBattleMoveDamage = (gBattleMoveDamage * 150) / 100;
        if(gBattleTypeFlags & BATTLE_TYPE_TRAINER && B_TRAINER_EXP_MULTIPLIER <= GEN_7)
            gBattleMoveDamage = (gBattleMoveDamage * 150) / 100;
    }

    It seems to work, because now each pkmn gets a different EXP value, but does this seem like a reasonable solution to you?
     
    Last edited:
    146
    Posts
    16
    Years
    • Age 26
    • Seen Apr 29, 2024
    It seems to work, because now each pkmn gets a different EXP value, but does this seem like a reasonable solution to you?

    Funny enough, I saw your post in the Battle Engine thread just a few minutes and I was working on fixing this myself! Great job figuring out what's causing it, this seems like a great solution. I'll be sure to credit you and add this to the OP. Thank you so much!
     
    55
    Posts
    14
    Years
    • Seen Feb 1, 2023
    Funny enough, I saw your post in the Battle Engine thread just a few minutes and I was working on fixing this myself! Great job figuring out what's causing it, this seems like a great solution. I'll be sure to credit you and add this to the OP. Thank you so much!

    Awesome!

    The one thing I couldn't figure out is why the 2nd enemy pkmn gives 200 EXP instead of the value I was expecting (half of my pkmn's level * 100). Any guess as to what could be causing that?

    Also, if you're gonna add that code to the top post (and feel free to do so!), I should point out that I modified the original AvgTeamLevel and renamed it.
    Spoiler:

    The reason I did it is because I also want to have dynamic enemy levels, per this post, and a pure average is too easy to break :P

    Keep up the good work!
     
    Last edited:
    146
    Posts
    16
    Years
    • Age 26
    • Seen Apr 29, 2024
    The one thing I couldn't figure out is why the 2nd enemy pkmn gives 200 EXP instead of the value I was expecting (half of my pkmn's level * 100). Any guess as to what could be causing that?

    Hmm... I'm not sure. In my testing, both Pokemon gave out numbers around what I expected.
    Something you might want to try is changing u16 to double in your getPkmnExpMultiplier method. When it was u16, if I was above the cap I was gaining 0 experience. Might not fix your issue, but it's a start/

    Also, if you're gonna add that code to the top post (and feel free to do so!), I should point out that I modified the original AvgTeamLevel and renamed it.

    Oh, awesome work! This is great. I'll be editing the OP with your code (slightly modified, if that's okay) right now.
     
    55
    Posts
    14
    Years
    • Seen Feb 1, 2023
    Hmm... I'm not sure. In my testing, both Pokemon gave out numbers around what I expected.
    Something you might want to try is changing u16 to double in your getPkmnExpMultiplier method. When it was u16, if I was above the cap I was gaining 0 experience. Might not fix your issue, but it's a start

    Oh, awesome work! This is great. I'll be editing the OP with your code (slightly modified, if that's okay) right now.


    Haha yes, I was about to comment that something was wrong! I was just testing and thought it was odd that I was getting 0 EXP.

    Of course, feel free to improve your post with whatever you need! The last thing I want is someone copying the code and it not working because i had changed the name of the function 😅

    Edit:
    Just confirmed the changes I added seem to fix the 200 EXP issue 😀. Could be some sort of bug in the battle code that sets the (player's) pkmn level to 2 when computing EXP for the 2nd enemy...?
     
    Last edited:
    146
    Posts
    16
    Years
    • Age 26
    • Seen Apr 29, 2024
    Haha yes, I was about to comment that something was wrong! I was just testing and thought it was odd that I was getting 0 EXP.

    Of course, feel free to improve your post with whatever you need! The last thing I want is someone copying the code and it not working because i had changed the name of the function 😅

    Just updated the post with some slightly modified versions of your methods. Thank you again for your help, I greatly appreciate it! Hopefully you hunt down your specific issue. I would try to help more, but it's 2:30am here and I have work in the morning, haha.
     
    55
    Posts
    14
    Years
    • Seen Feb 1, 2023
    Just updated the post with some slightly modified versions of your methods. Thank you again for your help, I greatly appreciate it! Hopefully you hunt down your specific issue. I would try to help more, but it's 2:30am here and I have work in the morning, haha.

    Yeah, it's 2:30 for me as well, haha. See my edit above for my theory about the 2nd EXP calculation 😀

    You're welcome! Tracking it down was pretty fun, after all.
     
    146
    Posts
    16
    Years
    • Age 26
    • Seen Apr 29, 2024
    Not sure if this is the right place to do this--but if I could request that this thread be moved to the new Tutorials and Resources section, that would be greatly appreciated. I couldn't find a way to do it myself, and this thread is a tutorial, not a help thread.
    Thank you.
     

    Lunos

    Random Uruguayan User
    3,114
    Posts
    15
    Years
  • Not sure if this is the right place to do this--but if I could request that this thread be moved to the new Tutorials and Resources section, that would be greatly appreciated. I couldn't find a way to do it myself, and this thread is a tutorial, not a help thread.
    Thank you.
    Moving threads is something only a member of the staff can do. I suggest you to contact one of them directly by leaving them a Visitor Message or a Private Message requesting them to move the thread over.
    The members of the staff in charge of both ROM Hacking areas (Binary and Decomps) are Avara, Pia Carrot and Bloodless.
     
    2
    Posts
    3
    Years
    • Seen Dec 6, 2020
    This thread's probably dead so this is a long shot, but could anyone help me out? I can't find the file src\battle_scripts_commands.c and a bunch of other lines seem to be missing. Does this still work with the current release of CFRU?
     
    Back
    Top