diff --git a/Source/msg.cpp b/Source/msg.cpp index c379ba4a7..2bdb8648a 100644 --- a/Source/msg.cpp +++ b/Source/msg.cpp @@ -1950,7 +1950,7 @@ size_t OnPlayerLevel(const TCmd *pCmd, size_t pnum) if (gbBufferMsgs != 1) { Player &player = Players[pnum]; - if (playerLevel <= MaxCharacterLevel && &player != MyPlayer) + if (playerLevel <= player.getMaxCharacterLevel() && &player != MyPlayer) player.setCharacterLevel(static_cast(playerLevel)); } else { SendPacket(pnum, &message, sizeof(message)); @@ -2225,7 +2225,7 @@ size_t OnCheatExperience(const TCmd *pCmd, size_t pnum) // NOLINT(misc-unused-pa #ifdef _DEBUG if (gbBufferMsgs == 1) SendPacket(pnum, pCmd, sizeof(*pCmd)); - else if (Players[pnum].getCharacterLevel() < MaxCharacterLevel) { + else if (!Players[pnum].isMaxCharacterLevel()) { Players[pnum]._pExperience = Players[pnum].getNextExperienceThreshold(); if (*sgOptions.Gameplay.experienceBar) { RedrawEverything(); diff --git a/Source/pack.cpp b/Source/pack.cpp index 837770d07..7fbcec063 100644 --- a/Source/pack.cpp +++ b/Source/pack.cpp @@ -107,12 +107,12 @@ bool IsCreationFlagComboValid(uint16_t iCreateInfo) return true; } -bool IsTownItemValid(uint16_t iCreateInfo) +bool IsTownItemValid(uint16_t iCreateInfo, const Player &player) { const uint8_t level = iCreateInfo & CF_LEVEL; const bool isBoyItem = (iCreateInfo & CF_BOY) != 0; - if (isBoyItem && level <= MaxCharacterLevel) + if (isBoyItem && level <= player.getMaxCharacterLevel()) return true; return level <= 30; @@ -180,7 +180,7 @@ bool UnPackNetItem(const Player &player, const ItemNetPack &packedItem, Item &it uint32_t dwBuff = SDL_SwapLE16(packedItem.item.dwBuff); ValidateField(creationFlags, IsCreationFlagComboValid(creationFlags)); if ((creationFlags & CF_TOWN) != 0) - ValidateField(creationFlags, IsTownItemValid(creationFlags)); + ValidateField(creationFlags, IsTownItemValid(creationFlags, player)); else if ((creationFlags & CF_USEFUL) == CF_UPER15) ValidateFields(creationFlags, dwBuff, IsUniqueMonsterItemValid(creationFlags, dwBuff)); else @@ -498,7 +498,7 @@ bool UnPackNetPlayer(const PlayerNetPack &packed, Player &player) Point position { packed.px, packed.py }; ValidateFields(position.x, position.y, InDungeonBounds(position)); ValidateField(packed.plrlevel, packed.plrlevel < NUMLEVELS); - ValidateField(packed.pLevel, packed.pLevel >= 1 && packed.pLevel <= MaxCharacterLevel); + ValidateField(packed.pLevel, packed.pLevel >= 1 && packed.pLevel <= player.getMaxCharacterLevel()); int32_t baseHpMax = SDL_SwapLE32(packed.pMaxHPBase); int32_t baseHp = SDL_SwapLE32(packed.pHPBase); diff --git a/Source/panels/charpanel.cpp b/Source/panels/charpanel.cpp index 3d3b56dbc..f0527d504 100644 --- a/Source/panels/charpanel.cpp +++ b/Source/panels/charpanel.cpp @@ -134,7 +134,7 @@ PanelEntry panelEntries[] = { } }, { N_("Next level"), { TopRightLabelX, 80 }, 99, 198, []() { - if (InspectPlayer->getCharacterLevel() == MaxCharacterLevel) { + if (InspectPlayer->isMaxCharacterLevel()) { return StyledText { UiFlags::ColorWhitegold, std::string(_("None")) }; } uint32_t nextExperienceThreshold = InspectPlayer->getNextExperienceThreshold(); diff --git a/Source/player.cpp b/Source/player.cpp index 17e625262..85b297cf2 100644 --- a/Source/player.cpp +++ b/Source/player.cpp @@ -2059,7 +2059,12 @@ void Player::UpdatePreviewCelSprite(_cmd_id cmdId, Point point, uint16_t wParam1 void Player::setCharacterLevel(uint8_t level) { - this->_pLevel = std::clamp(level, 1U, MaxCharacterLevel); + this->_pLevel = std::clamp(level, 1U, getMaxCharacterLevel()); +} + +uint8_t Player::getMaxCharacterLevel() const +{ + return GetMaximumCharacterLevel(); } uint32_t Player::getNextExperienceThreshold() const @@ -2445,7 +2450,7 @@ void AddPlrExperience(Player &player, int lvl, int exp) if (&player != MyPlayer || player._pHitPoints <= 0) return; - if (player.getCharacterLevel() >= MaxCharacterLevel) { + if (player.isMaxCharacterLevel()) { return; } @@ -2459,17 +2464,17 @@ void AddPlrExperience(Player &player, int lvl, int exp) clampedExp = std::min({ clampedExp, /* level 1-5: */ player.getNextExperienceThreshold() / 20U, /* level 6-50: */ 200U * player.getCharacterLevel() }); } - const uint32_t MaxExperience = GetNextExperienceThresholdForLevel(MaxCharacterLevel); + const uint32_t maxExperience = GetNextExperienceThresholdForLevel(player.getMaxCharacterLevel()); - // Overflow is only possible if a kill grants more than (2^32-1 - MaxExperience) XP in one go, which doesn't happen in normal gameplay. Clamp to experience required to reach max level - player._pExperience = std::min(player._pExperience + clampedExp, MaxExperience); + // ensure we only add enough experience to reach the max experience cap so we don't overflow + player._pExperience += std::min(clampedExp, maxExperience - player._pExperience); if (*sgOptions.Gameplay.experienceBar) { RedrawEverything(); } // Increase player level if applicable - while (player.getCharacterLevel() < MaxCharacterLevel && player._pExperience >= player.getNextExperienceThreshold()) { + while (!player.isMaxCharacterLevel() && player._pExperience >= player.getNextExperienceThreshold()) { // NextPlrLevel increments character level which changes the next experience threshold NextPlrLevel(player); } diff --git a/Source/player.h b/Source/player.h index 05a7ea63c..c4ccdcb6d 100644 --- a/Source/player.h +++ b/Source/player.h @@ -31,7 +31,6 @@ namespace devilution { constexpr int InventoryGridCells = 40; constexpr int MaxBeltItems = 8; constexpr int MaxResistance = 75; -constexpr uint8_t MaxCharacterLevel = 50; constexpr uint8_t MaxSpellLevel = 15; constexpr int PlayerNameLength = 32; @@ -763,6 +762,13 @@ public: */ void setCharacterLevel(uint8_t level); + [[nodiscard]] uint8_t getMaxCharacterLevel() const; + + [[nodiscard]] bool isMaxCharacterLevel() const + { + return getCharacterLevel() >= getMaxCharacterLevel(); + } + [[nodiscard]] uint32_t getNextExperienceThreshold() const; /** @brief Checks if the player is on the same level as the local player (MyPlayer). */ diff --git a/Source/playerdat.cpp b/Source/playerdat.cpp index 4d0b7d298..a7e407989 100644 --- a/Source/playerdat.cpp +++ b/Source/playerdat.cpp @@ -18,6 +18,7 @@ namespace devilution { namespace { +constexpr uint8_t MaxCharacterLevel = 50; /** Specifies the experience point limit of each level. */ const std::array ExpLvlsTbl { 0, @@ -78,6 +79,11 @@ uint32_t GetNextExperienceThresholdForLevel(unsigned level) return ExpLvlsTbl[std::min(level, ExpLvlsTbl.size() - 1)]; } +uint8_t GetMaximumCharacterLevel() +{ + return MaxCharacterLevel; +} + const _sfx_id herosounds[enum_size::value][enum_size::value] = { // clang-format off { PS_WARR1, PS_WARR2, PS_WARR3, PS_WARR4, PS_WARR5, PS_WARR6, PS_WARR7, PS_WARR8, PS_WARR9, PS_WARR10, PS_WARR11, PS_WARR12, PS_WARR13, PS_WARR14, PS_WARR15, PS_WARR16, PS_WARR17, PS_WARR18, PS_WARR19, PS_WARR20, PS_WARR21, PS_WARR22, PS_WARR23, PS_WARR24, PS_WARR25, PS_WARR26, PS_WARR27, PS_WARR28, PS_WARR29, PS_WARR30, PS_WARR31, PS_WARR32, PS_WARR33, PS_WARR34, PS_WARR35, PS_WARR36, PS_WARR37, PS_WARR38, PS_WARR39, PS_WARR40, PS_WARR41, PS_WARR42, PS_WARR43, PS_WARR44, PS_WARR45, PS_WARR46, PS_WARR47, PS_WARR48, PS_WARR49, PS_WARR50, PS_WARR51, PS_WARR52, PS_WARR53, PS_WARR54, PS_WARR55, PS_WARR56, PS_WARR57, PS_WARR58, PS_WARR59, PS_WARR60, PS_WARR61, PS_WARR62, PS_WARR63, PS_WARR64, PS_WARR65, PS_WARR66, PS_WARR67, PS_WARR68, PS_WARR69, PS_WARR70, PS_WARR71, PS_WARR72, PS_WARR73, PS_WARR74, PS_WARR75, PS_WARR76, PS_WARR77, PS_WARR78, PS_WARR79, PS_WARR80, PS_WARR81, PS_WARR82, PS_WARR83, PS_WARR84, PS_WARR85, PS_WARR86, PS_WARR87, PS_WARR88, PS_WARR89, PS_WARR90, PS_WARR91, PS_WARR92, PS_WARR93, PS_WARR94, PS_WARR95, PS_WARR96B, PS_WARR97, PS_WARR98, PS_WARR99, PS_WARR100, PS_WARR101, PS_WARR102, PS_DEAD }, diff --git a/Source/playerdat.hpp b/Source/playerdat.hpp index 8a289bc82..1f7831dda 100644 --- a/Source/playerdat.hpp +++ b/Source/playerdat.hpp @@ -137,6 +137,7 @@ struct PlayerAnimData { extern const _sfx_id herosounds[enum_size::value][enum_size::value]; uint32_t GetNextExperienceThresholdForLevel(unsigned level); +uint8_t GetMaximumCharacterLevel(); extern const PlayerData PlayersData[]; extern const PlayerSpriteData PlayersSpriteData[]; extern const PlayerAnimData PlayersAnimData[]; diff --git a/Source/qol/xpbar.cpp b/Source/qol/xpbar.cpp index 3a0ddd3be..6f8d79d0d 100644 --- a/Source/qol/xpbar.cpp +++ b/Source/qol/xpbar.cpp @@ -76,15 +76,15 @@ void DrawXPBar(const Surface &out) RenderClxSprite(out, (*xpbarArt)[0], back); - const uint8_t charLevel = player.getCharacterLevel(); - - if (charLevel == MaxCharacterLevel) { + if (player.isMaxCharacterLevel()) { // Draw a nice golden bar for max level characters. DrawBar(out, position, BarWidth, GoldGradient); return; } + const uint8_t charLevel = player.getCharacterLevel(); + const uint64_t prevXp = GetNextExperienceThresholdForLevel(charLevel - 1); if (player._pExperience < prevXp) return; @@ -124,7 +124,7 @@ bool CheckXPBarInfo() AddPanelString(fmt::format(fmt::runtime(_("Level {:d}")), charLevel)); - if (charLevel == MaxCharacterLevel) { + if (player.isMaxCharacterLevel()) { // Show a maximum level indicator for max level players. InfoColor = UiFlags::ColorWhitegold;