@ -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<uint8_t>(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();
@ -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 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);
@ -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();
@ -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<uint8_t>(level, 1U, MaxCharacterLevel);
this->_pLevel = std::clamp<uint8_t>(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()) {
@ -2459,17 +2464,17 @@ void AddPlrExperience(Player &player, int lvl, int exp)
clampedExp = std::min<uint32_t>({ 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);
// 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);
@ -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). */
@ -18,6 +18,7 @@
namespace devilution {
namespace {
/** Specifies the experience point limit of each level. */
const std::array<uint32_t, MaxCharacterLevel> ExpLvlsTbl {
0,
@ -78,6 +79,11 @@ uint32_t GetNextExperienceThresholdForLevel(unsigned level)
return ExpLvlsTbl[std::min<size_t>(level, ExpLvlsTbl.size() - 1)];
uint8_t GetMaximumCharacterLevel()
return MaxCharacterLevel;
const _sfx_id herosounds[enum_size<HeroClass>::value][enum_size<HeroSpeech>::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 },
@ -137,6 +137,7 @@ struct PlayerAnimData {
extern const _sfx_id herosounds[enum_size<HeroClass>::value][enum_size<HeroSpeech>::value];
uint32_t GetNextExperienceThresholdForLevel(unsigned level);
uint8_t GetMaximumCharacterLevel();
extern const PlayerData PlayersData[];
extern const PlayerSpriteData PlayersSpriteData[];
extern const PlayerAnimData PlayersAnimData[];
@ -76,15 +76,15 @@ void DrawXPBar(const Surface &out)
RenderClxSprite(out, (*xpbarArt)[0], back);
const uint8_t charLevel = player.getCharacterLevel();
if (charLevel == MaxCharacterLevel) {
// Draw a nice golden bar for max level characters.
DrawBar(out, position, BarWidth, GoldGradient);
const uint64_t prevXp = GetNextExperienceThresholdForLevel(charLevel - 1);
if (player._pExperience < prevXp)
@ -124,7 +124,7 @@ bool CheckXPBarInfo()
AddPanelString(fmt::format(fmt::runtime(_("Level {:d}")), charLevel));
// Show a maximum level indicator for max level players.
InfoColor = UiFlags::ColorWhitegold;