Browse Source

Add getter/setter for character levels to ensure _pNextExper stays synced

pull/6504/head
ephphatha 3 years ago committed by Anders Jenbo
parent
commit
da76e131e4
  1. 2
      Source/control.cpp
  2. 2
      Source/diablo.cpp
  3. 2
      Source/discord/discord.cpp
  4. 68
      Source/items.cpp
  5. 12
      Source/levels/trigs.cpp
  6. 14
      Source/loadsave.cpp
  7. 104
      Source/missiles.cpp
  8. 4
      Source/monster.cpp
  9. 6
      Source/msg.cpp
  10. 10
      Source/multi.cpp
  11. 4
      Source/objects.cpp
  12. 8
      Source/pack.cpp
  13. 6
      Source/panels/charpanel.cpp
  14. 2
      Source/pfile.cpp
  15. 57
      Source/player.cpp
  16. 26
      Source/player.h
  17. 2
      Source/plrmsg.cpp
  18. 2
      Source/qol/chatlog.cpp
  19. 8
      Source/qol/xpbar.cpp
  20. 4
      Source/spells.cpp
  21. 4
      Source/stores.cpp
  22. 4
      Source/towners.cpp
  23. 6
      test/player_test.cpp
  24. 2
      test/writehero_test.cpp

2
Source/control.cpp

@ -1205,7 +1205,7 @@ void DrawInfoBox(const Surface &out)
InfoColor = UiFlags::ColorWhitegold;
auto &target = Players[pcursplr];
InfoString = std::string_view(target._pName);
AddPanelString(fmt::format(fmt::runtime(_("{:s}, Level: {:d}")), _(PlayersData[static_cast<std::size_t>(target._pClass)].className), target._pLevel));
AddPanelString(fmt::format(fmt::runtime(_("{:s}, Level: {:d}")), _(PlayersData[static_cast<std::size_t>(target._pClass)].className), target.getCharacterLevel()));
AddPanelString(fmt::format(fmt::runtime(_("Hit Points {:d} of {:d}")), target._pHitPoints >> 6, target._pMaxHP >> 6));
}
}

2
Source/diablo.cpp

@ -2493,7 +2493,7 @@ bool TryIconCurs()
DoRepair(myPlayer, pcursinvitem);
else if (pcursstashitem != StashStruct::EmptyCell) {
Item &item = Stash.stashList[pcursstashitem];
RepairItem(item, myPlayer._pLevel);
RepairItem(item, myPlayer.getCharacterLevel());
}
NewCursor(CURSOR_HAND);
return true;

2
Source/discord/discord.cpp

@ -137,7 +137,7 @@ void UpdateGame()
return;
auto newData = PlayerData {
leveltype, setlvlnum, currlevel, MyPlayer->_pLevel, MyPlayer->_pgfxnum
leveltype, setlvlnum, currlevel, MyPlayer->getCharacterLevel(), MyPlayer->_pgfxnum
};
if (newData != tracked_data) {
tracked_data = newData;

68
Source/items.cpp

@ -2623,6 +2623,8 @@ void CalcPlrItemVals(Player &player, bool loadgfx)
}
}
const uint8_t playerLevel = player.getCharacterLevel();
if (mind == 0 && maxd == 0) {
mind = 1;
maxd = 1;
@ -2636,20 +2638,20 @@ void CalcPlrItemVals(Player &player, bool loadgfx)
}
if (player._pClass == HeroClass::Monk) {
mind = std::max(mind, player._pLevel / 2);
maxd = std::max(maxd, (int)player._pLevel);
mind = std::max(mind, playerLevel / 2);
maxd = std::max<int>(maxd, playerLevel);
}
}
if (HasAnyOf(player._pSpellFlags, SpellFlag::RageActive)) {
sadd += 2 * player._pLevel;
dadd += player._pLevel + player._pLevel / 2;
vadd += 2 * player._pLevel;
sadd += 2 * playerLevel;
dadd += playerLevel + playerLevel / 2;
vadd += 2 * playerLevel;
}
if (HasAnyOf(player._pSpellFlags, SpellFlag::RageCooldown)) {
sadd -= 2 * player._pLevel;
dadd -= player._pLevel + player._pLevel / 2;
vadd -= 2 * player._pLevel;
sadd -= 2 * playerLevel;
dadd -= playerLevel + playerLevel / 2;
vadd -= 2 * playerLevel;
}
player._pIMinDam = mind;
@ -2677,29 +2679,29 @@ void CalcPlrItemVals(Player &player, bool loadgfx)
player._pVitality = std::max(0, vadd + player._pBaseVit);
if (player._pClass == HeroClass::Rogue) {
player._pDamageMod = player._pLevel * (player._pStrength + player._pDexterity) / 200;
player._pDamageMod = playerLevel * (player._pStrength + player._pDexterity) / 200;
} else if (player._pClass == HeroClass::Monk) {
player._pDamageMod = player._pLevel * (player._pStrength + player._pDexterity) / 150;
player._pDamageMod = playerLevel * (player._pStrength + player._pDexterity) / 150;
if ((!player.InvBody[INVLOC_HAND_LEFT].isEmpty() && player.InvBody[INVLOC_HAND_LEFT]._itype != ItemType::Staff) || (!player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvBody[INVLOC_HAND_RIGHT]._itype != ItemType::Staff))
player._pDamageMod /= 2; // Monks get half the normal damage bonus if they're holding a non-staff weapon
} else if (player._pClass == HeroClass::Bard) {
if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Sword || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Sword)
player._pDamageMod = player._pLevel * (player._pStrength + player._pDexterity) / 150;
player._pDamageMod = playerLevel * (player._pStrength + player._pDexterity) / 150;
else if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Bow || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Bow) {
player._pDamageMod = player._pLevel * (player._pStrength + player._pDexterity) / 250;
player._pDamageMod = playerLevel * (player._pStrength + player._pDexterity) / 250;
} else {
player._pDamageMod = player._pLevel * player._pStrength / 100;
player._pDamageMod = playerLevel * player._pStrength / 100;
}
} else if (player._pClass == HeroClass::Barbarian) {
if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Axe || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Axe) {
player._pDamageMod = player._pLevel * player._pStrength / 75;
player._pDamageMod = playerLevel * player._pStrength / 75;
} else if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Mace || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Mace) {
player._pDamageMod = player._pLevel * player._pStrength / 75;
player._pDamageMod = playerLevel * player._pStrength / 75;
} else if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Bow || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Bow) {
player._pDamageMod = player._pLevel * player._pStrength / 300;
player._pDamageMod = playerLevel * player._pStrength / 300;
} else {
player._pDamageMod = player._pLevel * player._pStrength / 100;
player._pDamageMod = playerLevel * player._pStrength / 100;
}
if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Shield || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Shield) {
@ -2708,11 +2710,11 @@ void CalcPlrItemVals(Player &player, bool loadgfx)
else if (player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Shield)
player._pIAC -= player.InvBody[INVLOC_HAND_RIGHT]._iAC / 2;
} else if (IsNoneOf(player.InvBody[INVLOC_HAND_LEFT]._itype, ItemType::Staff, ItemType::Bow) && IsNoneOf(player.InvBody[INVLOC_HAND_RIGHT]._itype, ItemType::Staff, ItemType::Bow)) {
player._pDamageMod += player._pLevel * player._pVitality / 100;
player._pDamageMod += playerLevel * player._pVitality / 100;
}
player._pIAC += player._pLevel / 4;
player._pIAC += playerLevel / 4;
} else {
player._pDamageMod = player._pLevel * player._pStrength / 100;
player._pDamageMod = playerLevel * player._pStrength / 100;
}
player._pISpells = spl;
@ -2723,15 +2725,15 @@ void CalcPlrItemVals(Player &player, bool loadgfx)
player._pIEnAc = enac;
if (player._pClass == HeroClass::Barbarian) {
mr += player._pLevel;
fr += player._pLevel;
lr += player._pLevel;
mr += playerLevel;
fr += playerLevel;
lr += playerLevel;
}
if (HasAnyOf(player._pSpellFlags, SpellFlag::RageCooldown)) {
mr -= player._pLevel;
fr -= player._pLevel;
lr -= player._pLevel;
mr -= playerLevel;
fr -= playerLevel;
lr -= playerLevel;
}
if (HasAnyOf(iflgs, ItemSpecialEffect::ZeroResistance)) {
@ -2833,18 +2835,18 @@ void CalcPlrItemVals(Player &player, bool loadgfx)
PlayerArmorGraphic animArmorId = PlayerArmorGraphic::Light;
if (player.InvBody[INVLOC_CHEST]._itype == ItemType::HeavyArmor && player.InvBody[INVLOC_CHEST]._iStatFlag) {
if (player._pClass == HeroClass::Monk && player.InvBody[INVLOC_CHEST]._iMagical == ITEM_QUALITY_UNIQUE)
player._pIAC += player._pLevel / 2;
player._pIAC += playerLevel / 2;
animArmorId = PlayerArmorGraphic::Heavy;
} else if (player.InvBody[INVLOC_CHEST]._itype == ItemType::MediumArmor && player.InvBody[INVLOC_CHEST]._iStatFlag) {
if (player._pClass == HeroClass::Monk) {
if (player.InvBody[INVLOC_CHEST]._iMagical == ITEM_QUALITY_UNIQUE)
player._pIAC += player._pLevel * 2;
player._pIAC += playerLevel * 2;
else
player._pIAC += player._pLevel / 2;
player._pIAC += playerLevel / 2;
}
animArmorId = PlayerArmorGraphic::Medium;
} else if (player._pClass == HeroClass::Monk) {
player._pIAC += player._pLevel * 2;
player._pIAC += playerLevel * 2;
}
const uint8_t gfxNum = static_cast<uint8_t>(animWeaponId) | static_cast<uint8_t>(animArmorId);
@ -3668,7 +3670,7 @@ void DoRepair(Player &player, int cii)
pi = &player.InvBody[cii];
}
RepairItem(*pi, player._pLevel);
RepairItem(*pi, player.getCharacterLevel());
CalcPlrInv(player, true);
}
@ -4241,7 +4243,7 @@ void SpawnSmith(int lvl)
void SpawnPremium(const Player &player)
{
int lvl = player._pLevel;
int lvl = player.getCharacterLevel();
int maxItems = gbIsHellfire ? SMITH_PREMIUM_ITEMS : 6;
if (numpremium < maxItems) {
for (int i = 0; i < maxItems; i++) {
@ -4842,7 +4844,7 @@ void RechargeItem(Item &item, Player &player)
return;
int r = GetSpellStaffLevel(item._iSpell);
r = GenerateRnd(player._pLevel / r) + 1;
r = GenerateRnd(player.getCharacterLevel() / r) + 1;
do {
item._iMaxCharges--;

12
Source/levels/trigs.cpp

@ -89,11 +89,11 @@ bool IsWarpOpen(dungeon_type type)
return true;
if (gbIsHellfire) {
if (type == DTYPE_CATACOMBS && myPlayer._pLevel >= 10)
if (type == DTYPE_CATACOMBS && myPlayer.getCharacterLevel() >= 10)
return true;
if (type == DTYPE_CAVES && myPlayer._pLevel >= 15)
if (type == DTYPE_CAVES && myPlayer.getCharacterLevel() >= 15)
return true;
if (type == DTYPE_HELL && myPlayer._pLevel >= 20)
if (type == DTYPE_HELL && myPlayer.getCharacterLevel() >= 20)
return true;
if (type == DTYPE_NEST && IsAnyOf(Quests[Q_FARMER]._qactive, QUEST_DONE, QUEST_HIVE_DONE))
return true;
@ -893,19 +893,19 @@ void CheckTriggers()
diablo_message abortflag;
auto position = myPlayer.position.tile;
if (trigs[i]._tlvl == 5 && myPlayer._pLevel < 8) {
if (trigs[i]._tlvl == 5 && myPlayer.getCharacterLevel() < 8) {
abort = true;
position.y += 1;
abortflag = EMSG_REQUIRES_LVL_8;
}
if (IsAnyOf(trigs[i]._tlvl, 9, 17) && myPlayer._pLevel < 13) {
if (IsAnyOf(trigs[i]._tlvl, 9, 17) && myPlayer.getCharacterLevel() < 13) {
abort = true;
position.x += 1;
abortflag = EMSG_REQUIRES_LVL_13;
}
if (IsAnyOf(trigs[i]._tlvl, 13, 21) && myPlayer._pLevel < 17) {
if (IsAnyOf(trigs[i]._tlvl, 13, 21) && myPlayer.getCharacterLevel() < 17) {
abort = true;
position.y += 1;
abortflag = EMSG_REQUIRES_LVL_17;

14
Source/loadsave.cpp

@ -424,13 +424,13 @@ void LoadPlayer(LoadHelper &file, Player &player)
player._pMaxManaBase = file.NextLE<int32_t>();
player._pMana = file.NextLE<int32_t>();
player._pMaxMana = file.NextLE<int32_t>();
file.Skip<int32_t>(); // Skip _pManaPer - always derived from mana and maxMana
player._pLevel = file.NextLE<uint8_t>();
file.Skip<uint8_t>(); // Skip _pMaxLevel - unused
file.Skip(2); // Alignment
file.Skip<int32_t>(); // Skip _pManaPer - always derived from mana and maxMana
player.setCharacterLevel(file.NextLE<uint8_t>()); // this sets _pNextExper as well.
file.Skip<uint8_t>(); // Skip _pMaxLevel - unused
file.Skip(2); // Alignment
player._pExperience = file.NextLE<uint32_t>();
file.Skip<uint32_t>(); // Skip _pMaxExp - unused
player._pNextExper = file.NextLE<uint32_t>(); // This can be calculated based on _pLevel
file.Skip<uint32_t>(); // Skip _pMaxExp - unused
file.Skip<uint32_t>(); // Skip _pNextExper, it was calculated based on _pLevel above
player._pArmorClass = file.NextLE<int8_t>();
player._pMagResist = file.NextLE<int8_t>();
player._pFireResist = file.NextLE<int8_t>();
@ -1235,7 +1235,7 @@ void SavePlayer(SaveHelper &file, const Player &player)
file.WriteLE<int32_t>(player._pMana);
file.WriteLE<int32_t>(player._pMaxMana);
file.Skip<int32_t>(); // Skip _pManaPer
file.WriteLE<uint8_t>(player._pLevel);
file.WriteLE<uint8_t>(player.getCharacterLevel());
file.Skip<uint8_t>(); // skip _pMaxLevel, this value is uninitialised in most cases in Diablo/Hellfire so there's no point setting it.
file.Skip(2); // Alignment
file.WriteLE<uint32_t>(player._pExperience);

104
Source/missiles.cpp

@ -333,7 +333,7 @@ bool Plr2PlrMHit(const Player &player, int p, int mindam, int maxdam, int dist,
- target.GetArmor();
} else {
hit = player.GetMagicToHit()
- (target._pLevel * 2)
- (target.getCharacterLevel() * 2)
- dist;
}
@ -348,7 +348,7 @@ bool Plr2PlrMHit(const Player &player, int p, int mindam, int maxdam, int dist,
blkper = GenerateRnd(100);
}
int blk = target.GetBlockChance() - (player._pLevel * 2);
int blk = target.GetBlockChance() - (player.getCharacterLevel() * 2);
blk = std::clamp(blk, 0, 100);
int dam;
@ -672,7 +672,7 @@ bool GuardianTryFireAt(Missile &missile, Point target)
return false;
Player &player = Players[missile._misource];
int dmg = GenerateRnd(10) + (player._pLevel / 2) + 1;
int dmg = GenerateRnd(10) + (player.getCharacterLevel() / 2) + 1;
dmg = ScaleSpellEffect(dmg, missile._mispllvl);
Direction dir = GetDirection(position, target);
@ -795,14 +795,14 @@ DamageRange GetDamageAmt(SpellID spell, int spellLevel)
case SpellID::HealOther:
/// BUGFIX: healing calculation is unused
return {
AddClassHealingBonus(myPlayer._pLevel + spellLevel + 1, myPlayer._pClass) - 1,
AddClassHealingBonus((4 * myPlayer._pLevel) + (6 * spellLevel) + 10, myPlayer._pClass) - 1
AddClassHealingBonus(myPlayer.getCharacterLevel() + spellLevel + 1, myPlayer._pClass) - 1,
AddClassHealingBonus((4 * myPlayer.getCharacterLevel()) + (6 * spellLevel) + 10, myPlayer._pClass) - 1
};
case SpellID::RuneOfLight:
case SpellID::Lightning:
return { 2, 2 + myPlayer._pLevel };
return { 2, 2 + myPlayer.getCharacterLevel() };
case SpellID::Flash: {
int min = ScaleSpellEffect(myPlayer._pLevel, spellLevel);
int min = ScaleSpellEffect(myPlayer.getCharacterLevel(), spellLevel);
min += min / 2;
return { min, min * 2 };
};
@ -833,28 +833,28 @@ DamageRange GetDamageAmt(SpellID spell, int spellLevel)
case SpellID::FireWall:
case SpellID::LightningWall:
case SpellID::RingOfFire: {
const int min = 2 * myPlayer._pLevel + 4;
const int min = 2 * myPlayer.getCharacterLevel() + 4;
return { min, min + 36 };
}
case SpellID::Fireball:
case SpellID::RuneOfFire: {
const int base = (2 * myPlayer._pLevel) + 4;
const int base = (2 * myPlayer.getCharacterLevel()) + 4;
return {
ScaleSpellEffect(base, spellLevel),
ScaleSpellEffect(base + 36, spellLevel)
};
} break;
case SpellID::Guardian: {
const int base = (myPlayer._pLevel / 2) + 1;
const int base = (myPlayer.getCharacterLevel() / 2) + 1;
return {
ScaleSpellEffect(base, spellLevel),
ScaleSpellEffect(base + 9, spellLevel)
};
} break;
case SpellID::ChainLightning:
return { 4, 4 + (2 * myPlayer._pLevel) };
return { 4, 4 + (2 * myPlayer.getCharacterLevel()) };
case SpellID::FlameWave: {
const int min = 6 * (myPlayer._pLevel + 1);
const int min = 6 * (myPlayer.getCharacterLevel() + 1);
return { min, min + 54 };
}
case SpellID::Nova:
@ -862,28 +862,28 @@ DamageRange GetDamageAmt(SpellID spell, int spellLevel)
case SpellID::RuneOfImmolation:
case SpellID::RuneOfNova:
return {
ScaleSpellEffect((myPlayer._pLevel + 5) / 2, spellLevel) * 5,
ScaleSpellEffect((myPlayer._pLevel + 30) / 2, spellLevel) * 5
ScaleSpellEffect((myPlayer.getCharacterLevel() + 5) / 2, spellLevel) * 5,
ScaleSpellEffect((myPlayer.getCharacterLevel() + 30) / 2, spellLevel) * 5
};
case SpellID::Inferno: {
int max = myPlayer._pLevel + 4;
int max = myPlayer.getCharacterLevel() + 4;
max += max / 2;
return { 3, max };
}
case SpellID::Golem:
return { 11, 17 };
case SpellID::Apocalypse:
return { myPlayer._pLevel, myPlayer._pLevel * 6 };
return { myPlayer.getCharacterLevel(), myPlayer.getCharacterLevel() * 6 };
case SpellID::Elemental:
/// BUGFIX: Divide min and max by 2
return {
ScaleSpellEffect(2 * myPlayer._pLevel + 4, spellLevel),
ScaleSpellEffect(2 * myPlayer._pLevel + 40, spellLevel)
ScaleSpellEffect(2 * myPlayer.getCharacterLevel() + 4, spellLevel),
ScaleSpellEffect(2 * myPlayer.getCharacterLevel() + 40, spellLevel)
};
case SpellID::ChargedBolt:
return { 1, 1 + (myPlayer._pMagic / 4) };
case SpellID::HolyBolt:
return { myPlayer._pLevel + 9, myPlayer._pLevel + 18 };
return { myPlayer.getCharacterLevel() + 9, myPlayer.getCharacterLevel() + 18 };
case SpellID::BloodStar: {
const int min = (myPlayer._pMagic / 2) + 3 * spellLevel - (myPlayer._pMagic / 8);
return { min, min };
@ -999,14 +999,14 @@ bool PlayerMHit(int pnum, Monster *monster, int dist, int mind, int maxd, Missil
int tac = player.GetArmor();
if (monster != nullptr) {
hper = monster->toHit
+ ((monster->level(sgGameInitInfo.nDifficulty) - player._pLevel) * 2)
+ ((monster->level(sgGameInitInfo.nDifficulty) - player.getCharacterLevel()) * 2)
+ 30
- (dist * 2) - tac;
} else {
hper = 100 - (tac / 2) - (dist * 2);
}
} else if (monster != nullptr) {
hper += (monster->level(sgGameInitInfo.nDifficulty) * 2) - (player._pLevel * 2) - (dist * 2);
hper += (monster->level(sgGameInitInfo.nDifficulty) * 2) - (player.getCharacterLevel() * 2) - (dist * 2);
}
int minhit = 10;
@ -1030,7 +1030,7 @@ bool PlayerMHit(int pnum, Monster *monster, int dist, int mind, int maxd, Missil
int blkper = player.GetBlockChance(false);
if (monster != nullptr)
blkper -= (monster->level(sgGameInitInfo.nDifficulty) - player._pLevel) * 2;
blkper -= (monster->level(sgGameInitInfo.nDifficulty) - player.getCharacterLevel()) * 2;
blkper = std::clamp(blkper, 0, 100);
int8_t resper;
@ -1168,7 +1168,7 @@ void AddRuneOfFire(Missile &missile, AddMissileParameter &parameter)
void AddRuneOfLight(Missile &missile, AddMissileParameter &parameter)
{
int lvl = (missile.sourceType() == MissileSource::Player) ? missile.sourcePlayer()->_pLevel : 0;
int lvl = (missile.sourceType() == MissileSource::Player) ? missile.sourcePlayer()->getCharacterLevel() : 0;
int dmg = 16 * (GenerateRndSum(10, 2) + lvl + 2);
missile._midam = dmg;
AddRune(missile, parameter.dst, MissileID::LightningWall);
@ -1198,7 +1198,7 @@ void AddReflect(Missile &missile, AddMissileParameter & /*parameter*/)
Player &player = *missile.sourcePlayer();
int add = (missile._mispllvl != 0 ? missile._mispllvl : 2) * player._pLevel;
int add = (missile._mispllvl != 0 ? missile._mispllvl : 2) * player.getCharacterLevel();
if (player.wReflections + add >= std::numeric_limits<uint16_t>::max())
add = 0;
player.wReflections += add;
@ -1399,9 +1399,9 @@ void AddSpectralArrow(Missile &missile, AddMissileParameter &parameter)
const Player &player = *missile.sourcePlayer();
if (player._pClass == HeroClass::Rogue)
av += (player._pLevel - 1) / 4;
av += (player.getCharacterLevel() - 1) / 4;
else if (player._pClass == HeroClass::Warrior || player._pClass == HeroClass::Bard)
av += (player._pLevel - 1) / 8;
av += (player.getCharacterLevel() - 1) / 8;
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::QuickAttack))
av++;
@ -1518,7 +1518,7 @@ void AddLightningWall(Missile &missile, AddMissileParameter &parameter)
void AddBigExplosion(Missile &missile, AddMissileParameter & /*parameter*/)
{
if (missile.sourceType() == MissileSource::Player) {
int dmg = 2 * (missile.sourcePlayer()->_pLevel + GenerateRndSum(10, 2)) + 4;
int dmg = 2 * (missile.sourcePlayer()->getCharacterLevel() + GenerateRndSum(10, 2)) + 4;
dmg = ScaleSpellEffect(dmg, missile._mispllvl);
missile._midam = dmg;
@ -1572,7 +1572,7 @@ void AddMana(Missile &missile, AddMissileParameter & /*parameter*/)
Player &player = Players[missile._misource];
int manaAmount = (GenerateRnd(10) + 1) << 6;
for (int i = 0; i < player._pLevel; i++) {
for (int i = 0; i < player.getCharacterLevel(); i++) {
manaAmount += (GenerateRnd(4) + 1) << 6;
}
for (int i = 0; i < missile._mispllvl; i++) {
@ -1617,7 +1617,7 @@ void AddSearch(Missile &missile, AddMissileParameter & /*parameter*/)
AutoMapShowItems = true;
int lvl = 2;
if (missile._misource >= 0)
lvl = player._pLevel * 2;
lvl = player.getCharacterLevel() * 2;
missile._mirange = lvl + 10 * missile._mispllvl + 245;
for (auto &other : Missiles) {
@ -1661,9 +1661,9 @@ void AddElementalArrow(Missile &missile, AddMissileParameter &parameter)
if (missile._micaster == TARGET_MONSTERS) {
const Player &player = Players[missile._misource];
if (player._pClass == HeroClass::Rogue)
av += (player._pLevel) / 4;
av += (player.getCharacterLevel()) / 4;
else if (IsAnyOf(player._pClass, HeroClass::Warrior, HeroClass::Bard))
av += (player._pLevel) / 8;
av += (player.getCharacterLevel()) / 8;
if (gbIsHellfire) {
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::QuickAttack))
@ -1702,9 +1702,9 @@ void AddArrow(Missile &missile, AddMissileParameter &parameter)
av = GenerateRnd(32) + 16;
}
if (player._pClass == HeroClass::Rogue)
av += (player._pLevel - 1) / 4;
av += (player.getCharacterLevel() - 1) / 4;
else if (player._pClass == HeroClass::Warrior || player._pClass == HeroClass::Bard)
av += (player._pLevel - 1) / 8;
av += (player.getCharacterLevel() - 1) / 8;
if (gbIsHellfire) {
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::QuickAttack))
@ -1873,7 +1873,7 @@ void AddNovaBall(Missile &missile, AddMissileParameter &parameter)
void AddFireWall(Missile &missile, AddMissileParameter &parameter)
{
missile._midam = GenerateRndSum(10, 2) + 2;
missile._midam += missile._misource >= 0 ? Players[missile._misource]._pLevel : currlevel; // BUGFIX: missing parenthesis around ternary (fixed)
missile._midam += missile._misource >= 0 ? Players[missile._misource].getCharacterLevel() : currlevel; // BUGFIX: missing parenthesis around ternary (fixed)
missile._midam <<= 3;
UpdateMissileVelocity(missile, parameter.dst, 16);
int i = missile._mispllvl;
@ -1897,7 +1897,7 @@ void AddFireball(Missile &missile, AddMissileParameter &parameter)
sp += std::min(missile._mispllvl * 2, 34);
Player &player = Players[missile._misource];
int dmg = 2 * (player._pLevel + GenerateRndSum(10, 2)) + 4;
int dmg = 2 * (player.getCharacterLevel() + GenerateRndSum(10, 2)) + 4;
missile._midam = ScaleSpellEffect(dmg, missile._mispllvl);
}
UpdateMissileVelocity(missile, dst, sp);
@ -2035,7 +2035,7 @@ void AddFlashBottom(Missile &missile, AddMissileParameter & /*parameter*/)
switch (missile.sourceType()) {
case MissileSource::Player: {
Player &player = *missile.sourcePlayer();
int dmg = GenerateRndSum(20, player._pLevel + 1) + player._pLevel + 1;
int dmg = GenerateRndSum(20, player.getCharacterLevel() + 1) + player.getCharacterLevel() + 1;
missile._midam = ScaleSpellEffect(dmg, missile._mispllvl);
missile._midam += missile._midam / 2;
} break;
@ -2054,7 +2054,7 @@ void AddFlashTop(Missile &missile, AddMissileParameter & /*parameter*/)
{
if (missile._micaster == TARGET_MONSTERS) {
if (!missile.IsTrap()) {
int dmg = Players[missile._misource]._pLevel + 1;
int dmg = Players[missile._misource].getCharacterLevel() + 1;
dmg += GenerateRndSum(20, dmg);
missile._midam = ScaleSpellEffect(dmg, missile._mispllvl);
missile._midam += missile._midam / 2;
@ -2084,7 +2084,7 @@ void AddManaShield(Missile &missile, AddMissileParameter &parameter)
void AddFlameWave(Missile &missile, AddMissileParameter &parameter)
{
missile._midam = GenerateRnd(10) + Players[missile._misource]._pLevel + 1;
missile._midam = GenerateRnd(10) + Players[missile._misource].getCharacterLevel() + 1;
UpdateMissileVelocity(missile, parameter.dst, 16);
missile._mirange = 255;
@ -2132,7 +2132,7 @@ void AddGuardian(Missile &missile, AddMissileParameter &parameter)
missile.position.start = *spawnPosition;
missile._mlid = AddLight(missile.position.tile, 1);
missile._mirange = missile._mispllvl + (player._pLevel / 2);
missile._mirange = missile._mispllvl + (player.getCharacterLevel() / 2);
if (missile._mirange > 30)
missile._mirange = 30;
@ -2363,7 +2363,7 @@ void AddHealing(Missile &missile, AddMissileParameter & /*parameter*/)
Player &player = Players[missile._misource];
int hp = GenerateRnd(10) + 1;
hp += GenerateRndSum(4, player._pLevel) + player._pLevel;
hp += GenerateRndSum(4, player.getCharacterLevel()) + player.getCharacterLevel();
hp += GenerateRndSum(6, missile._mispllvl) + missile._mispllvl;
hp <<= 6;
@ -2401,7 +2401,7 @@ void AddElemental(Missile &missile, AddMissileParameter &parameter)
Player &player = Players[missile._misource];
int dmg = 2 * (player._pLevel + GenerateRndSum(10, 2)) + 4;
int dmg = 2 * (player.getCharacterLevel() + GenerateRndSum(10, 2)) + 4;
missile._midam = ScaleSpellEffect(dmg, missile._mispllvl) / 2;
UpdateMissileVelocity(missile, dst, 16);
@ -2477,7 +2477,7 @@ void AddNova(Missile &missile, AddMissileParameter &parameter)
if (!missile.IsTrap()) {
Player &player = Players[missile._misource];
int dmg = GenerateRndSum(6, 5) + player._pLevel + 5;
int dmg = GenerateRndSum(6, 5) + player.getCharacterLevel() + 5;
missile._midam = ScaleSpellEffect(dmg / 2, missile._mispllvl);
} else {
missile._midam = (currlevel / 2) + GenerateRndSum(3, 3);
@ -2490,17 +2490,17 @@ void AddRage(Missile &missile, AddMissileParameter &parameter)
{
Player &player = Players[missile._misource];
if (HasAnyOf(player._pSpellFlags, SpellFlag::RageActive | SpellFlag::RageCooldown) || player._pHitPoints <= player._pLevel << 6) {
if (HasAnyOf(player._pSpellFlags, SpellFlag::RageActive | SpellFlag::RageCooldown) || player._pHitPoints <= player.getCharacterLevel() << 6) {
missile._miDelFlag = true;
parameter.spellFizzled = true;
return;
}
int tmp = 3 * player._pLevel;
int tmp = 3 * player.getCharacterLevel();
tmp <<= 7;
player._pSpellFlags |= SpellFlag::RageActive;
missile.var2 = tmp;
int lvl = player._pLevel * 2;
int lvl = player.getCharacterLevel() * 2;
missile._mirange = lvl + 10 * missile._mispllvl + 245;
CalcPlrItemVals(player, true);
RedrawEverything();
@ -2567,7 +2567,7 @@ void AddApocalypse(Missile &missile, AddMissileParameter & /*parameter*/)
missile.var4 = std::max(missile.position.start.x - 8, 1);
missile.var5 = std::min(missile.position.start.x + 8, MAXDUNX - 1);
missile.var6 = missile.var4;
int playerLevel = player._pLevel;
int playerLevel = player.getCharacterLevel();
missile._midam = GenerateRndSum(6, playerLevel) + playerLevel;
missile._mirange = 255;
}
@ -2582,7 +2582,7 @@ void AddInferno(Missile &missile, AddMissileParameter &parameter)
missile._mirange = missile.var2 + 20;
missile._mlid = AddLight(missile.position.start, 1);
if (missile._micaster == TARGET_MONSTERS) {
int i = GenerateRnd(Players[missile._misource]._pLevel) + GenerateRnd(2);
int i = GenerateRnd(Players[missile._misource].getCharacterLevel()) + GenerateRnd(2);
missile._midam = 8 * i + 16 + ((8 * i + 16) / 2);
} else {
auto &monster = Monsters[missile._misource];
@ -2639,7 +2639,7 @@ void AddHolyBolt(Missile &missile, AddMissileParameter &parameter)
missile.var1 = missile.position.start.x;
missile.var2 = missile.position.start.y;
missile._mlid = AddLight(missile.position.start, 8);
missile._midam = GenerateRnd(10) + player._pLevel + 9;
missile._midam = GenerateRnd(10) + player.getCharacterLevel() + 9;
}
void AddResurrect(Missile &missile, AddMissileParameter & /*parameter*/)
@ -3139,7 +3139,7 @@ void ProcessRingOfFire(Missile &missile)
{
missile._miDelFlag = true;
int8_t src = missile._misource;
uint8_t lvl = missile._micaster == TARGET_MONSTERS ? Players[src]._pLevel : currlevel;
uint8_t lvl = missile._micaster == TARGET_MONSTERS ? Players[src].getCharacterLevel() : currlevel;
int dmg = 16 * (GenerateRndSum(10, 2) + lvl + 2) / 2;
if (missile.limitReached)
@ -3189,7 +3189,7 @@ void ProcessLightningWallControl(Missile &missile)
}
int id = missile._misource;
int lvl = !missile.IsTrap() ? Players[id]._pLevel : 0;
int lvl = !missile.IsTrap() ? Players[id].getCharacterLevel() : 0;
int dmg = 16 * (GenerateRndSum(10, 2) + lvl + 2);
{
@ -3301,7 +3301,7 @@ void ProcessLightningControl(Missile &missile)
dam = GenerateRnd(currlevel) + 2 * currlevel;
} else if (missile._micaster == TARGET_MONSTERS) {
// BUGFIX: damage of missile should be encoded in missile struct; player can be dead/have left the game before missile arrives.
dam = (GenerateRnd(2) + GenerateRnd(Players[missile._misource]._pLevel) + 2) << 6;
dam = (GenerateRnd(2) + GenerateRnd(Players[missile._misource].getCharacterLevel()) + 2) << 6;
} else {
auto &monster = Monsters[missile._misource];
dam = 2 * (monster.minDamage + GenerateRnd(monster.maxDamage - monster.minDamage + 1));
@ -3857,7 +3857,7 @@ void ProcessRage(Missile &missile)
if (HasAnyOf(player._pSpellFlags, SpellFlag::RageActive)) {
player._pSpellFlags &= ~SpellFlag::RageActive;
player._pSpellFlags |= SpellFlag::RageCooldown;
int lvl = player._pLevel * 2;
int lvl = player.getCharacterLevel() * 2;
missile._mirange = lvl + 10 * missile._mispllvl + 245;
} else {
player._pSpellFlags &= ~SpellFlag::RageCooldown;

4
Source/monster.cpp

@ -1159,7 +1159,7 @@ void MonsterAttackPlayer(Monster &monster, Player &player, int hit, int minDam,
ac += 40;
if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::ACAgainstUndead) && monster.data().monsterClass == MonsterClass::Undead)
ac += 20;
hit += 2 * (monster.level(sgGameInitInfo.nDifficulty) - player._pLevel)
hit += 2 * (monster.level(sgGameInitInfo.nDifficulty) - player.getCharacterLevel())
+ 30
- ac;
int minhit = GetMinHit();
@ -4571,7 +4571,7 @@ void SpawnGolem(Player &player, Monster &golem, Point position, Missile &missile
golem.maxHitPoints = 2 * (320 * missile._mispllvl + player._pMaxMana / 3);
golem.hitPoints = golem.maxHitPoints;
golem.armorClass = 25;
golem.toHit = 5 * (missile._mispllvl + 8) + 2 * player._pLevel;
golem.toHit = 5 * (missile._mispllvl + 8) + 2 * player.getCharacterLevel();
golem.minDamage = 2 * (missile._mispllvl + 4);
golem.maxDamage = 2 * (missile._mispllvl + 8);
golem.flags |= MFLAG_GOLEM;

6
Source/msg.cpp

@ -1951,7 +1951,7 @@ size_t OnPlayerLevel(const TCmd *pCmd, size_t pnum)
if (gbBufferMsgs != 1) {
Player &player = Players[pnum];
if (playerLevel <= MaxCharacterLevel && &player != MyPlayer)
player._pLevel = static_cast<uint8_t>(playerLevel);
player.setCharacterLevel(static_cast<uint8_t>(playerLevel));
} else {
SendPacket(pnum, &message, sizeof(message));
}
@ -2026,7 +2026,7 @@ size_t OnPlayerJoinLevel(const TCmd *pCmd, size_t pnum)
ResetPlayerGFX(player);
player.plractive = true;
gbActivePlayers++;
EventPlrMsg(fmt::format(fmt::runtime(_("Player '{:s}' (level {:d}) just joined the game")), player._pName, player._pLevel));
EventPlrMsg(fmt::format(fmt::runtime(_("Player '{:s}' (level {:d}) just joined the game")), player._pName, player.getCharacterLevel()));
}
if (player.plractive && &player != MyPlayer) {
@ -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]._pLevel < MaxCharacterLevel) {
else if (Players[pnum].getCharacterLevel() < MaxCharacterLevel) {
Players[pnum]._pExperience = Players[pnum]._pNextExper;
if (*sgOptions.Gameplay.experienceBar) {
RedrawEverything();

10
Source/multi.cpp

@ -153,9 +153,9 @@ void NetReceivePlayerData(TPkt *pkt)
bool IsNetPlayerValid(const Player &player)
{
return player._pLevel >= 1
&& player._pLevel <= MaxCharacterLevel
&& static_cast<uint8_t>(player._pClass) < enum_size<HeroClass>::value
// we no longer check character level here, players with out-of-range clevels are not allowed to join the game and we don't observe change clevel messages that would set it out of range
// (there's no code path that would result in _pLevel containing an out of range value in the DevilutionX code)
return static_cast<uint8_t>(player._pClass) < enum_size<HeroClass>::value
&& player.plrlevel < NUMLEVELS
&& InDungeonBounds(player.position.tile)
&& !std::string_view(player._pName).empty();
@ -794,7 +794,7 @@ bool NetInit(bool bSinglePlayer)
Player &myPlayer = *MyPlayer;
// separator for marking messages from a different game
AddMessageToChatLog(_("New Game"), nullptr, UiFlags::ColorRed);
AddMessageToChatLog(fmt::format(fmt::runtime(_("Player '{:s}' (level {:d}) just joined the game")), myPlayer._pName, myPlayer._pLevel));
AddMessageToChatLog(fmt::format(fmt::runtime(_("Player '{:s}' (level {:d}) just joined the game")), myPlayer._pName, myPlayer.getCharacterLevel()));
return true;
}
@ -849,7 +849,7 @@ void recv_plrinfo(int pnum, const TCmdPlrInfoHdr &header, bool recv)
} else {
szEvent = _("Player '{:s}' (level {:d}) is already in the game");
}
EventPlrMsg(fmt::format(fmt::runtime(szEvent), player._pName, player._pLevel));
EventPlrMsg(fmt::format(fmt::runtime(szEvent), player._pName, player.getCharacterLevel()));
SyncInitPlr(player);

4
Source/objects.cpp

@ -2850,7 +2850,7 @@ void OperateShrineMendicant(Player &player)
return;
int gold = player._pGold / 2;
AddPlrExperience(player, player._pLevel, gold);
AddPlrExperience(player, player.getCharacterLevel(), gold);
TakePlrsMoney(gold);
RedrawEverything();
@ -2868,7 +2868,7 @@ void OperateShrineSparkling(Player &player, Point spawnPosition)
if (&player != MyPlayer)
return;
AddPlrExperience(player, player._pLevel, 1000 * currlevel);
AddPlrExperience(player, player.getCharacterLevel(), 1000 * currlevel);
AddMissile(
spawnPosition,

8
Source/pack.cpp

@ -255,7 +255,7 @@ void PackPlayer(PlayerPack &packed, const Player &player)
packed.pBaseMag = player._pBaseMag;
packed.pBaseDex = player._pBaseDex;
packed.pBaseVit = player._pBaseVit;
packed.pLevel = player._pLevel;
packed.pLevel = player.getCharacterLevel();
packed.pStatPts = player._pStatPts;
packed.pExperience = SDL_SwapLE32(player._pExperience);
packed.pGold = SDL_SwapLE32(player._pGold);
@ -300,7 +300,7 @@ void PackNetPlayer(PlayerNetPack &packed, const Player &player)
packed.pBaseMag = player._pBaseMag;
packed.pBaseDex = player._pBaseDex;
packed.pBaseVit = player._pBaseVit;
packed.pLevel = player._pLevel;
packed.pLevel = player.getCharacterLevel();
packed.pStatPts = player._pStatPts;
packed.pExperience = SDL_SwapLE32(player._pExperience);
packed.pHPBase = SDL_SwapLE32(player._pHPBase);
@ -421,7 +421,7 @@ void UnPackPlayer(const PlayerPack &packed, Player &player)
Point position { packed.px, packed.py };
player = {};
player._pLevel = std::clamp<uint8_t>(packed.pLevel, 1, MaxCharacterLevel);
player.setCharacterLevel(packed.pLevel);
player._pMaxHPBase = SDL_SwapLE32(packed.pMaxHPBase);
player._pHPBase = SDL_SwapLE32(packed.pHPBase);
player._pHPBase = std::clamp<int32_t>(player._pHPBase, 0, player._pMaxHPBase);
@ -515,7 +515,7 @@ bool UnPackNetPlayer(const PlayerNetPack &packed, Player &player)
ValidateField(packed._pNumInv, packed._pNumInv < InventoryGridCells);
player._pLevel = packed.pLevel;
player.setCharacterLevel(packed.pLevel);
player.position.tile = position;
player.position.future = position;
player.plrlevel = packed.plrlevel;

6
Source/panels/charpanel.cpp

@ -126,7 +126,7 @@ PanelEntry panelEntries[] = {
[]() { return StyledText { UiFlags::ColorWhite, std::string(_(PlayersData[static_cast<std::size_t>(InspectPlayer->_pClass)].className)) }; } },
{ N_("Level"), { 57, 52 }, 57, 45,
[]() { return StyledText { UiFlags::ColorWhite, StrCat(InspectPlayer->_pLevel) }; } },
[]() { return StyledText { UiFlags::ColorWhite, StrCat(InspectPlayer->getCharacterLevel()) }; } },
{ N_("Experience"), { TopRightLabelX, 52 }, 99, 91,
[]() {
int spacing = ((InspectPlayer->_pExperience >= 1000000000) ? 0 : 1);
@ -134,7 +134,7 @@ PanelEntry panelEntries[] = {
} },
{ N_("Next level"), { TopRightLabelX, 80 }, 99, 198,
[]() {
if (InspectPlayer->_pLevel == MaxCharacterLevel) {
if (InspectPlayer->getCharacterLevel() == MaxCharacterLevel) {
return StyledText { UiFlags::ColorWhitegold, std::string(_("None")) };
}
int spacing = ((InspectPlayer->_pNextExper >= 1000000000) ? 0 : 1);
@ -168,7 +168,7 @@ PanelEntry panelEntries[] = {
[]() { return StyledText { UiFlags::ColorWhite, FormatInteger(InspectPlayer->_pGold) }; } },
{ N_("Armor class"), { RightColumnLabelX, 163 }, 57, RightColumnLabelWidth,
[]() { return StyledText { GetValueColor(InspectPlayer->_pIBonusAC), StrCat(InspectPlayer->GetArmor() + InspectPlayer->_pLevel * 2) }; } },
[]() { return StyledText { GetValueColor(InspectPlayer->_pIBonusAC), StrCat(InspectPlayer->GetArmor() + InspectPlayer->getCharacterLevel() * 2) }; } },
{ N_("To hit"), { RightColumnLabelX, 191 }, 57, RightColumnLabelWidth,
[]() { return StyledText { GetValueColor(InspectPlayer->_pIBonusToHit), StrCat(InspectPlayer->InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Bow ? InspectPlayer->GetRangedToHit() : InspectPlayer->GetMeleeToHit(), "%") }; } },
{ N_("Damage"), { RightColumnLabelX, 219 }, 57, RightColumnLabelWidth,

2
Source/pfile.cpp

@ -171,7 +171,7 @@ void CopySaveFile(uint32_t saveNum, std::string targetPath)
void Game2UiPlayer(const Player &player, _uiheroinfo *heroinfo, bool bHasSaveFile)
{
CopyUtf8(heroinfo->name, player._pName, sizeof(heroinfo->name));
heroinfo->level = player._pLevel;
heroinfo->level = player.getCharacterLevel();
heroinfo->heroclass = player._pClass;
heroinfo->strength = player._pStrength;
heroinfo->magic = player._pMagic;

57
Source/player.cpp

@ -578,10 +578,10 @@ bool PlrHitMonst(Player &player, Monster &monster, bool adjacentDamage = false)
return false;
if (adjacentDamage) {
if (player._pLevel > 20)
if (player.getCharacterLevel() > 20)
hper -= 30;
else
hper -= (35 - player._pLevel) * 2;
hper -= (35 - player.getCharacterLevel()) * 2;
}
int hit = GenerateRnd(100);
@ -614,7 +614,7 @@ bool PlrHitMonst(Player &player, Monster &monster, bool adjacentDamage = false)
int dam2 = dam << 6;
dam += player._pDamageMod;
if (player._pClass == HeroClass::Warrior || player._pClass == HeroClass::Barbarian) {
if (GenerateRnd(100) < player._pLevel) {
if (GenerateRnd(100) < player.getCharacterLevel()) {
dam *= 2;
}
}
@ -761,7 +761,7 @@ bool PlrHitPlr(Player &attacker, Player &target)
blk = GenerateRnd(100);
}
int blkper = target.GetBlockChance() - (attacker._pLevel * 2);
int blkper = target.GetBlockChance() - (attacker.getCharacterLevel() * 2);
blkper = std::clamp(blkper, 0, 100);
if (hit >= hper) {
@ -781,7 +781,7 @@ bool PlrHitPlr(Player &attacker, Player &target)
dam += attacker._pIBonusDamMod + attacker._pDamageMod;
if (attacker._pClass == HeroClass::Warrior || attacker._pClass == HeroClass::Barbarian) {
if (GenerateRnd(100) < attacker._pLevel) {
if (GenerateRnd(100) < attacker.getCharacterLevel()) {
dam *= 2;
}
}
@ -1474,8 +1474,9 @@ void ValidatePlayer()
assert(MyPlayer != nullptr);
Player &myPlayer = *MyPlayer;
if (myPlayer._pLevel > MaxCharacterLevel)
myPlayer._pLevel = MaxCharacterLevel;
// Player::setCharacterLevel both ensures that the player level is within the expected range and sets _pNextExpr to the appropriate value for their next level up
myPlayer.setCharacterLevel(myPlayer.getCharacterLevel());
// This lets us catch cases where someone is editing experience directly through memory modification and reset their experience back to the expected cap.
if (myPlayer._pExperience > myPlayer._pNextExper) {
myPlayer._pExperience = myPlayer._pNextExper;
if (*sgOptions.Gameplay.experienceBar) {
@ -2056,16 +2057,22 @@ 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->_pNextExper = GetNextExperienceThresholdForLevel(this->getCharacterLevel());
}
int32_t Player::calculateBaseLife() const
{
const PlayerData &playerData = PlayersData[static_cast<size_t>(_pClass)];
return playerData.adjLife + (playerData.lvlLife * _pLevel) + (playerData.chrLife * _pBaseVit);
return playerData.adjLife + (playerData.lvlLife * getCharacterLevel()) + (playerData.chrLife * _pBaseVit);
}
int32_t Player::calculateBaseMana() const
{
const PlayerData &playerData = PlayersData[static_cast<size_t>(_pClass)];
return playerData.adjMana + (playerData.lvlMana * _pLevel) + (playerData.chrMana * _pBaseMag);
return playerData.adjMana + (playerData.lvlMana * getCharacterLevel()) + (playerData.chrMana * _pBaseMag);
}
Player *PlayerAtPosition(Point position)
@ -2280,7 +2287,7 @@ void CreatePlayer(Player &player, HeroClass c)
const PlayerData &playerData = PlayersData[static_cast<size_t>(c)];
player._pLevel = 1;
player.setCharacterLevel(1);
player._pClass = c;
player._pBaseStr = playerData.baseStr;
@ -2308,7 +2315,6 @@ void CreatePlayer(Player &player, HeroClass c)
player._pMaxManaBase = player._pMana;
player._pExperience = 0;
player._pNextExper = GetNextExperienceThresholdForLevel(player._pLevel);
player._pArmorClass = 0;
player._pLightRad = 10;
player._pInfraFlag = false;
@ -2388,7 +2394,7 @@ int CalcStatDiff(Player &player)
void NextPlrLevel(Player &player)
{
player._pLevel++;
player.setCharacterLevel(player.getCharacterLevel() + 1);
CalcPlrInv(player, true);
@ -2397,8 +2403,6 @@ void NextPlrLevel(Player &player)
} else {
player._pStatPts += 5;
}
player._pNextExper = GetNextExperienceThresholdForLevel(player._pLevel);
int hp = PlayersData[static_cast<size_t>(player._pClass)].lvlLife;
player._pMaxHP += hp;
@ -2437,22 +2441,18 @@ void AddPlrExperience(Player &player, int lvl, int exp)
if (&player != MyPlayer || player._pHitPoints <= 0)
return;
if (player._pLevel >= MaxCharacterLevel) {
player._pLevel = MaxCharacterLevel;
if (player.getCharacterLevel() >= MaxCharacterLevel) {
return;
}
// Adjust xp based on difference in level between player and monster
uint32_t clampedExp = std::max(static_cast<int>(exp * (1 + (lvl - player._pLevel) / 10.0)), 0);
uint32_t clampedExp = std::max(static_cast<int>(exp * (1 + (lvl - player.getCharacterLevel()) / 10.0)), 0);
// Prevent power leveling
if (gbIsMultiplayer) {
// Use a minimum of 1 so level 0 characters can still gain experience
const uint32_t clampedPlayerLevel = std::max<uint32_t>(player._pLevel, 1);
// for low level characters experience gain is capped to 1/20 of current levels xp
// for high level characters experience gain is capped to 200 * current level - this is a smaller value than 1/20 of the exp needed for the next level after level 5.
clampedExp = std::min({ clampedExp, /* level 1-5: */ GetNextExperienceThresholdForLevel(clampedPlayerLevel) / 20U, /* level 6-50: */ 200U * clampedPlayerLevel });
clampedExp = std::min<uint32_t>({ clampedExp, /* level 1-5: */ GetNextExperienceThresholdForLevel(player.getCharacterLevel()) / 20U, /* level 6-50: */ 200U * player.getCharacterLevel() });
}
const uint32_t MaxExperience = GetNextExperienceThresholdForLevel(MaxCharacterLevel);
@ -2465,17 +2465,17 @@ void AddPlrExperience(Player &player, int lvl, int exp)
}
// Increase player level if applicable
unsigned newLvl = player._pLevel;
unsigned newLvl = player.getCharacterLevel();
while (newLvl < MaxCharacterLevel && player._pExperience >= GetNextExperienceThresholdForLevel(newLvl)) {
newLvl++;
}
if (newLvl != player._pLevel) {
for (unsigned i = newLvl - player._pLevel; i > 0; i--) {
if (newLvl != player.getCharacterLevel()) {
for (unsigned i = newLvl - player.getCharacterLevel(); i > 0; i--) {
NextPlrLevel(player);
}
}
NetSendCmdParam1(false, CMD_PLRLEVEL, player._pLevel);
NetSendCmdParam1(false, CMD_PLRLEVEL, player.getCharacterLevel());
}
void AddPlrMonstExper(int lvl, int exp, char pmask)
@ -2548,7 +2548,6 @@ void InitPlayer(Player &player, bool firstTime)
SpellID s = PlayersData[static_cast<size_t>(player._pClass)].skill;
player._pAblSpells = GetSpellBitmask(s);
player._pNextExper = GetNextExperienceThresholdForLevel(player._pLevel);
player._pInvincible = false;
if (&player == MyPlayer) {
@ -2663,10 +2662,10 @@ void StartPlrHit(Player &player, int dam, bool forcehit)
RedrawComponent(PanelDrawComponent::Health);
if (player._pClass == HeroClass::Barbarian) {
if (dam >> 6 < player._pLevel + player._pLevel / 4 && !forcehit) {
if (dam >> 6 < player.getCharacterLevel() + player.getCharacterLevel() / 4 && !forcehit) {
return;
}
} else if (dam >> 6 < player._pLevel && !forcehit) {
} else if (dam >> 6 < player.getCharacterLevel() && !forcehit) {
return;
}
@ -2781,7 +2780,7 @@ StartPlayerKill(Player &player, DeathReason deathReason)
ear._iCreateInfo = player._pName[0] << 8 | player._pName[1];
ear._iSeed = player._pName[2] << 24 | player._pName[3] << 16 | player._pName[4] << 8 | player._pName[5];
ear._ivalue = player._pLevel;
ear._ivalue = player.getCharacterLevel();
if (FindGetItem(ear._iSeed, IDI_EAR, ear._iCreateInfo) == -1) {
DeadItem(player, std::move(ear), { 0, 0 });

26
Source/player.h

@ -318,7 +318,11 @@ struct Player {
ActorPosition position;
Direction _pdir; // Direction faced by player (direction enum)
HeroClass _pClass;
uint8_t _pLevel;
private:
uint8_t _pLevel = 1; // Use get/setCharacterLevel as this attribute is tied to _pNextExper
public:
uint8_t _pgfxnum; // Bitmask indicating what variant of the sprite the player is using. The 3 lower bits define weapon (PlayerWeaponGraphic) and the higher bits define armour (starting with PlayerArmorGraphic)
int8_t _pISplLvlAdd;
/** @brief Specifies whether players are in non-PvP mode. */
@ -510,7 +514,7 @@ struct Player {
*/
int GetMeleeToHit() const
{
int hper = _pLevel + _pDexterity / 2 + _pIBonusToHit + BaseHitChance;
int hper = getCharacterLevel() + _pDexterity / 2 + _pIBonusToHit + BaseHitChance;
if (_pClass == HeroClass::Warrior)
hper += 20;
return hper;
@ -533,7 +537,7 @@ struct Player {
*/
int GetRangedToHit() const
{
int hper = _pLevel + _pDexterity + _pIBonusToHit + BaseHitChance;
int hper = getCharacterLevel() + _pDexterity + _pIBonusToHit + BaseHitChance;
if (_pClass == HeroClass::Rogue)
hper += 20;
else if (_pClass == HeroClass::Warrior || _pClass == HeroClass::Bard)
@ -571,7 +575,7 @@ struct Player {
{
int blkper = _pDexterity + _pBaseToBlk;
if (useLevel)
blkper += _pLevel * 2;
blkper += getCharacterLevel() * 2;
return blkper;
}
@ -749,6 +753,20 @@ struct Player {
*/
void UpdatePreviewCelSprite(_cmd_id cmdId, Point point, uint16_t wParam1, uint16_t wParam2);
[[nodiscard]] uint8_t getCharacterLevel() const
{
return _pLevel;
}
/**
* @brief Sets the character level and derived attributes
*
* This method ensures the level is within the allowed range and sets the number of experience points
* required for the next character level as needed.
* @param level New character level
*/
void setCharacterLevel(uint8_t level);
/** @brief Checks if the player is on the same level as the local player (MyPlayer). */
bool isOnActiveLevel() const
{

2
Source/plrmsg.cpp

@ -82,7 +82,7 @@ void SendPlrMsg(Player &player, std::string_view text)
{
PlayerMessage &message = GetNextMessage();
std::string from = fmt::format(fmt::runtime(_("{:s} (lvl {:d}): ")), player._pName, player._pLevel);
std::string from = fmt::format(fmt::runtime(_("{:s} (lvl {:d}): ")), player._pName, player.getCharacterLevel());
message.style = UiFlags::ColorWhite;
message.time = SDL_GetTicks();

2
Source/qol/chatlog.cpp

@ -127,7 +127,7 @@ void AddMessageToChatLog(std::string_view message, Player *player, UiFlags flags
if (player == nullptr) {
ChatLogLines.emplace_back(MultiColoredText { "{0} {1}", { { timestamp, UiFlags::ColorRed }, { std::string(message), flags } } });
} else {
std::string playerInfo = fmt::format(fmt::runtime(_("{:s} (lvl {:d}): ")), player->_pName, player->_pLevel);
std::string playerInfo = fmt::format(fmt::runtime(_("{:s} (lvl {:d}): ")), player->_pName, player->getCharacterLevel());
ChatLogLines.emplace_back(MultiColoredText { std::string(message), { {} }, 20 });
UiFlags nameColor = player == MyPlayer ? UiFlags::ColorWhitegold : UiFlags::ColorBlue;
ChatLogLines.emplace_back(MultiColoredText { "{0} - {1}", { { timestamp, UiFlags::ColorRed }, { playerInfo, nameColor } } });

8
Source/qol/xpbar.cpp

@ -76,7 +76,7 @@ void DrawXPBar(const Surface &out)
RenderClxSprite(out, (*xpbarArt)[0], back);
const uint8_t charLevel = player._pLevel;
const uint8_t charLevel = player.getCharacterLevel();
if (charLevel == MaxCharacterLevel) {
// Draw a nice golden bar for max level characters.
@ -120,7 +120,7 @@ bool CheckXPBarInfo()
const Player &player = *MyPlayer;
const uint8_t charLevel = player._pLevel;
const uint8_t charLevel = player.getCharacterLevel();
AddPanelString(fmt::format(fmt::runtime(_("Level {:d}")), charLevel));
@ -137,8 +137,8 @@ bool CheckXPBarInfo()
InfoColor = UiFlags::ColorWhite;
AddPanelString(fmt::format(fmt::runtime(_("Experience: {:s}")), FormatInteger(player._pExperience)));
AddPanelString(fmt::format(fmt::runtime(_("Next Level: {:s}")), FormatInteger(GetNextExperienceThresholdForLevel(charLevel))));
AddPanelString(fmt::format(fmt::runtime(_("{:s} to Level {:d}")), FormatInteger(GetNextExperienceThresholdForLevel(charLevel) - player._pExperience), charLevel + 1));
AddPanelString(fmt::format(fmt::runtime(_("Next Level: {:s}")), FormatInteger(player._pNextExper)));
AddPanelString(fmt::format(fmt::runtime(_("{:s} to Level {:d}")), FormatInteger(player._pNextExper - player._pExperience), charLevel + 1));
return true;
}

4
Source/spells.cpp

@ -121,7 +121,7 @@ int GetManaAmount(const Player &player, SpellID sn)
}
if (sn == SpellID::Healing || sn == SpellID::HealOther) {
ma = (GetSpellData(SpellID::Healing).sManaCost + 2 * player._pLevel - adj);
ma = (GetSpellData(SpellID::Healing).sManaCost + 2 * player.getCharacterLevel() - adj);
} else if (GetSpellData(sn).sManaCost == 255) {
ma = (player._pMaxManaBase >> 6) - adj;
} else {
@ -282,7 +282,7 @@ void DoHealOther(const Player &caster, Player &target)
}
int hp = (GenerateRnd(10) + 1) << 6;
for (unsigned i = 0; i < caster._pLevel; i++) {
for (unsigned i = 0; i < caster.getCharacterLevel(); i++) {
hp += (GenerateRnd(4) + 1) << 6;
}
for (int i = 0; i < caster.GetSpellLevel(SpellID::HealOther); i++) {

4
Source/stores.cpp

@ -2124,7 +2124,7 @@ void SetupTownStores()
{
Player &myPlayer = *MyPlayer;
int l = myPlayer._pLevel / 2;
int l = myPlayer.getCharacterLevel() / 2;
if (!gbIsMultiplayer) {
l = 0;
for (int i = 0; i < NUMLEVELS; i++) {
@ -2139,7 +2139,7 @@ void SetupTownStores()
SpawnSmith(l);
SpawnWitch(l);
SpawnHealer(l);
SpawnBoy(myPlayer._pLevel);
SpawnBoy(myPlayer.getCharacterLevel());
SpawnPremium(myPlayer);
}

4
Source/towners.cpp

@ -622,7 +622,7 @@ void TalkToFarmer(Player &player, Towner &farmer)
break;
}
if (!player._pLvlVisited[9] && player._pLevel < 15) {
if (!player._pLvlVisited[9] && player.getCharacterLevel() < 15) {
_speech_id qt = TEXT_FARMER8;
if (player._pLvlVisited[2])
qt = TEXT_FARMER5;
@ -713,7 +713,7 @@ void TalkToCowFarmer(Player &player, Towner &cowFarmer)
NetSendCmdQuest(true, quest);
break;
case QUEST_HIVE_ACTIVE:
if (!player._pLvlVisited[9] && player._pLevel < 15) {
if (!player._pLvlVisited[9] && player.getCharacterLevel() < 15) {
_speech_id qt = TEXT_JERSEY12;
switch (GenerateRnd(4)) {
case 0:

6
test/player_test.cpp

@ -14,7 +14,9 @@ int RunBlockTest(int frames, ItemSpecialEffect flags)
player._pHFrames = frames;
player._pIFlags = flags;
StartPlrHit(player, 5, false);
// StartPlrHit compares damage (a 6 bit fixed point value) to character level to determine if the player shrugs off the hit.
// We don't initialise player so this comparison can't be relied on, instead we use forcehit to ensure the player enters hit mode
StartPlrHit(player, 0, true);
int i = 1;
for (; i < 100; i++) {
@ -109,7 +111,7 @@ static void AssertPlayer(Player &player)
ASSERT_EQ(player._pDexterity, 30);
ASSERT_EQ(player._pBaseVit, 20);
ASSERT_EQ(player._pVitality, 20);
ASSERT_EQ(player._pLevel, 1);
ASSERT_EQ(player.getCharacterLevel(), 1);
ASSERT_EQ(player._pStatPts, 0);
ASSERT_EQ(player._pExperience, 0);
ASSERT_EQ(player._pGold, 100);

2
test/writehero_test.cpp

@ -278,7 +278,7 @@ void AssertPlayer(Player &player)
ASSERT_EQ(player._pDexterity, 281);
ASSERT_EQ(player._pBaseVit, 80);
ASSERT_EQ(player._pVitality, 90);
ASSERT_EQ(player._pLevel, 50);
ASSERT_EQ(player.getCharacterLevel(), 50);
ASSERT_EQ(player._pStatPts, 0);
ASSERT_EQ(player._pExperience, 1583495809);
ASSERT_EQ(player._pGold, 0);

Loading…
Cancel
Save