From c56acb944b3e669df37270a86f57f57c12223f64 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Tue, 9 Jan 2024 18:47:23 +0100 Subject: [PATCH] New Feature: Level Caps (#3632) * New Feature: Level Caps * B_LEVEL_CAP_EXP_UP and fixes * 1 exp fix for hard level caps * remove 1 exp hack * Reviews applied * fix u8/u16 --------- Co-authored-by: Bassoonian --- include/level_caps.h | 37 ++++++++++++++++++ src/battle_controller_player.c | 3 +- src/battle_interface.c | 3 +- src/battle_script_commands.c | 5 ++- src/daycare.c | 5 ++- src/level_caps.c | 71 ++++++++++++++++++++++++++++++++++ src/party_menu.c | 3 +- src/pokemon.c | 3 +- 8 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 include/level_caps.h create mode 100644 src/level_caps.c diff --git a/include/level_caps.h b/include/level_caps.h new file mode 100644 index 0000000000..ed70ac665b --- /dev/null +++ b/include/level_caps.h @@ -0,0 +1,37 @@ +#ifndef GUARD_LEVEL_CAP_H +#define GUARD_LEVEL_CAP_H + +// experience (soft-)caps + +#define EXP_CAP_NONE 0 // Regular behavior, no level caps are applied +#define EXP_CAP_HARD 1 // Pokémon with a level >= the level cap cannot gain any experience +#define EXP_CAP_SOFT 2 // Pokémon with a level >= the level cap will gain reduced experience + +#define LEVEL_CAP_NONE 0 // No level cap, only applicable if B_EXP_CAP_TYPE is EXP_CAP_NONE +#define LEVEL_CAP_FLAG_LIST 1 // Level cap is chosen according to the first unset flag in `sLevelCapFlagMap` +#define LEVEL_CAP_VARIABLE 2 // Level cap is chosen according to the contents of the event variable specified by B_LEVEL_CAP_VARIABLE + +#define B_EXP_CAP_TYPE EXP_CAP_NONE // [EXP_CAP_NONE, EXP_CAP_HARD, EXP_CAP_SOFT] choose the type of level cap to apply +#define B_LEVEL_CAP_TYPE LEVEL_CAP_NONE // [LEVEL_CAP_NONE, LEVEL_CAP_FLAG_LIST, LEVEL_CAP_VARIABLE] choose the method to derive the level cap +#define B_LEVEL_CAP_VARIABLE 0 // event variable used to derive level cap if B_LEVEL_CAP_TYPE is set to LEVEL_CAP_VARIABLE + +#define B_RARE_CANDY_CAP FALSE // If set to true, Rare Candies can't be used to go over the level cap +#define B_LEVEL_CAP_EXP_UP FALSE // If set to true, mons under level cap will receive more experience + +#if B_EXP_CAP_TYPE != EXP_CAP_NONE && B_EXP_CAP_TYPE != EXP_CAP_HARD && B_EXP_CAP_TYPE != EXP_CAP_SOFT +#error "Invalid choice for B_EXP_CAP_TYPE, must be of [EXP_CAP_NONE, EXP_CAP_HARD, EXP_CAP_SOFT]" +#endif + +#if B_EXP_CAP_TYPE == EXP_CAP_HARD || B_EXP_CAP_TYPE == EXP_CAP_SOFT +#if B_LEVEL_CAP_TYPE != LEVEL_CAP_FLAG_LIST && B_LEVEL_CAP_TYPE != LEVEL_CAP_VARIABLE +#error "Invalid choice for B_LEVEL_CAP_TYPE, must be of [LEVEL_CAP_FLAG_LIST, LEVEL_CAP_VARIABLE]" +#endif +#if B_LEVEL_CAP_TYPE == LEVEL_CAP_VARIABLE && B_LEVEL_CAP_VARIABLE == 0 +#error "B_LEVEL_CAP_TYPE set to LEVEL_CAP_VARIABLE, but no variable chosen for B_LEVEL_CAP_VARIABLE, set B_LEVEL_CAP_VARIABLE to a valid event variable" +#endif +#endif + +u32 GetCurrentLevelCap(void); +u32 GetSoftLevelCapExpValue(u32 level, u32 expValue); + +#endif /* GUARD_LEVEL_CAP_H */ diff --git a/src/battle_controller_player.c b/src/battle_controller_player.c index ec5945d633..ba16ef2dbb 100644 --- a/src/battle_controller_player.c +++ b/src/battle_controller_player.c @@ -39,6 +39,7 @@ #include "constants/songs.h" #include "constants/trainers.h" #include "constants/rgb.h" +#include "level_caps.h" static void PlayerBufferExecCompleted(u32 battler); static void PlayerHandleLoadMonSprite(u32 battler); @@ -2145,7 +2146,7 @@ void PlayerHandleExpUpdate(u32 battler) u8 monId = gBattleResources->bufferA[battler][1]; s32 taskId, expPointsToGive; - if (GetMonData(&gPlayerParty[monId], MON_DATA_LEVEL) >= MAX_LEVEL) + if (GetMonData(&gPlayerParty[monId], MON_DATA_LEVEL) >= GetCurrentLevelCap()) { PlayerBufferExecCompleted(battler); } diff --git a/src/battle_interface.c b/src/battle_interface.c index 3357fd9939..830c7f41cc 100644 --- a/src/battle_interface.c +++ b/src/battle_interface.c @@ -33,6 +33,7 @@ #include "constants/rgb.h" #include "constants/songs.h" #include "constants/items.h" +#include "level_caps.h" enum { // Corresponds to gHealthboxElementsGfxTable (and the tables after it) in graphics.c @@ -2687,7 +2688,7 @@ static void MoveBattleBarGraphically(u8 battlerId, u8 whichBar) &gBattleSpritesDataPtr->battleBars[battlerId].currValue, array, B_EXPBAR_PIXELS / 8); level = GetMonData(&gPlayerParty[gBattlerPartyIndexes[battlerId]], MON_DATA_LEVEL); - if (level == MAX_LEVEL) + if (level >= GetCurrentLevelCap()) { for (i = 0; i < 8; i++) array[i] = 0; diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 1624a9a7b3..b4ae183f76 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -27,6 +27,7 @@ #include "bg.h" #include "string_util.h" #include "pokemon_icon.h" +#include "level_caps.h" #include "m4a.h" #include "mail.h" #include "event_data.h" @@ -4239,14 +4240,14 @@ static void Cmd_getexp(void) if (IsValidForBattle(&gPlayerParty[*expMonId])) { if (wasSentOut) - gBattleMoveDamage = gBattleStruct->expValue; + gBattleMoveDamage = GetSoftLevelCapExpValue(gPlayerParty[*expMonId].level, gBattleStruct->expValue); else gBattleMoveDamage = 0; if ((holdEffect == HOLD_EFFECT_EXP_SHARE || IsGen6ExpShareEnabled()) && (B_SPLIT_EXP < GEN_6 || gBattleMoveDamage == 0)) // only give exp share bonus in later gens if the mon wasn't sent out { - gBattleMoveDamage += gBattleStruct->expShareExpValue; + gBattleMoveDamage += GetSoftLevelCapExpValue(gPlayerParty[*expMonId].level, gBattleStruct->expShareExpValue);; } ApplyExperienceMultipliers(&gBattleMoveDamage, *expMonId, gBattlerFainted); diff --git a/src/daycare.c b/src/daycare.c index e8426b4c00..6ecc4c7669 100644 --- a/src/daycare.c +++ b/src/daycare.c @@ -3,6 +3,7 @@ #include "battle.h" #include "daycare.h" #include "string_util.h" +#include "level_caps.h" #include "mail.h" #include "pokemon_storage_system.h" #include "event_data.h" @@ -316,7 +317,7 @@ static u16 TakeSelectedPokemonFromDaycare(struct DaycareMon *daycareMon) species = newSpecies; } - if (GetMonData(&pokemon, MON_DATA_LEVEL) != MAX_LEVEL) + if (GetMonData(&pokemon, MON_DATA_LEVEL) < GetCurrentLevelCap()) { experience = GetMonData(&pokemon, MON_DATA_EXP) + daycareMon->steps; SetMonData(&pokemon, MON_DATA_EXP, &experience); @@ -365,6 +366,8 @@ static u8 GetNumLevelsGainedFromSteps(struct DaycareMon *daycareMon) levelBefore = GetLevelFromBoxMonExp(&daycareMon->mon); levelAfter = GetLevelAfterDaycareSteps(&daycareMon->mon, daycareMon->steps); + if (levelAfter > GetCurrentLevelCap()) + levelAfter = GetCurrentLevelCap(); return levelAfter - levelBefore; } diff --git a/src/level_caps.c b/src/level_caps.c new file mode 100644 index 0000000000..9a51edccb0 --- /dev/null +++ b/src/level_caps.c @@ -0,0 +1,71 @@ +#include "global.h" +#include "battle.h" +#include "event_data.h" +#include "level_caps.h" +#include "pokemon.h" + + +u32 GetCurrentLevelCap(void) +{ + static const u32 sLevelCapFlagMap[][2] = + { + {FLAG_BADGE01_GET, 15}, + {FLAG_BADGE02_GET, 19}, + {FLAG_BADGE03_GET, 24}, + {FLAG_BADGE04_GET, 29}, + {FLAG_BADGE05_GET, 31}, + {FLAG_BADGE06_GET, 33}, + {FLAG_BADGE07_GET, 42}, + {FLAG_BADGE08_GET, 46}, + {FLAG_IS_CHAMPION, 58}, + }; + + u32 i; + + if (B_LEVEL_CAP_TYPE == LEVEL_CAP_FLAG_LIST) + { + for (i = 0; i < ARRAY_COUNT(sLevelCapFlagMap); i++) + { + if (!FlagGet(sLevelCapFlagMap[i][0])) + return sLevelCapFlagMap[i][1]; + } + } + else if (B_LEVEL_CAP_TYPE == LEVEL_CAP_VARIABLE) + { + return VarGet(B_LEVEL_CAP_VARIABLE); + } + + return MAX_LEVEL; +} + +u32 GetSoftLevelCapExpValue(u32 level, u32 expValue) +{ + static const u32 sExpScalingDown[5] = { 4, 8, 16, 32, 64 }; + static const u32 sExpScalingUp[5] = { 16, 8, 4, 2, 1 }; + + u32 levelDifference; + u32 currentLevelCap = GetCurrentLevelCap(); + + if (B_EXP_CAP_TYPE == EXP_CAP_NONE) + return expValue; + + if (B_LEVEL_CAP_EXP_UP && level < currentLevelCap) + { + levelDifference = currentLevelCap - level; + if (levelDifference > ARRAY_COUNT(sExpScalingDown)) + return expValue + (expValue / sExpScalingUp[ARRAY_COUNT(sExpScalingDown) - 1]); + else + return expValue + (expValue / sExpScalingUp[levelDifference]); + } + else if (B_EXP_CAP_TYPE == EXP_CAP_SOFT && level >= currentLevelCap) + { + levelDifference = level - currentLevelCap; + if (levelDifference > ARRAY_COUNT(sExpScalingDown)) + return expValue / sExpScalingDown[ARRAY_COUNT(sExpScalingDown) - 1]; + else + return expValue / sExpScalingDown[levelDifference]; + } + else + return 0; + +} diff --git a/src/party_menu.c b/src/party_menu.c index 4eced9536f..54d8ec41b7 100644 --- a/src/party_menu.c +++ b/src/party_menu.c @@ -31,6 +31,7 @@ #include "item.h" #include "item_menu.h" #include "item_use.h" +#include "level_caps.h" #include "link.h" #include "link_rfu.h" #include "mail.h" @@ -5512,7 +5513,7 @@ void ItemUseCB_RareCandy(u8 taskId, TaskFunc task) u8 holdEffectParam = ItemId_GetHoldEffectParam(*itemPtr); sInitialLevel = GetMonData(mon, MON_DATA_LEVEL); - if (sInitialLevel != MAX_LEVEL) + if (!(B_RARE_CANDY_CAP && sInitialLevel >= GetCurrentLevelCap())) { BufferMonStatsToTaskData(mon, arrayPtr); cannotUseEffect = ExecuteTableBasedItemEffect(mon, *itemPtr, gPartyMenu.slotId, 0); diff --git a/src/pokemon.c b/src/pokemon.c index ac70d41d1d..cb4a08366d 100644 --- a/src/pokemon.c +++ b/src/pokemon.c @@ -17,6 +17,7 @@ #include "field_weather.h" #include "graphics.h" #include "item.h" +#include "level_caps.h" #include "link.h" #include "main.h" #include "overworld.h" @@ -5054,7 +5055,7 @@ bool8 TryIncrementMonLevel(struct Pokemon *mon) expPoints = gExperienceTables[gSpeciesInfo[species].growthRate][MAX_LEVEL]; SetMonData(mon, MON_DATA_EXP, &expPoints); } - if (nextLevel > MAX_LEVEL || expPoints < gExperienceTables[gSpeciesInfo[species].growthRate][nextLevel]) + if (nextLevel > GetCurrentLevelCap() || expPoints < gExperienceTables[gSpeciesInfo[species].growthRate][nextLevel]) { return FALSE; }